* Swap AddressSummary for AddressRow * Update name/address on change * Move AddressRow to right
491 lines
12 KiB
TypeScript
491 lines
12 KiB
TypeScript
// Copyright 2017-2019 @polkadot/ui-app 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 { AccountId, AccountIndex, Address } from '@polkadot/types';
|
|
import { I18nProps } from './types';
|
|
|
|
import BN from 'bn.js';
|
|
import { Label } from 'semantic-ui-react';
|
|
import React from 'react';
|
|
import { withCalls } from '@polkadot/ui-api';
|
|
import { Button, Input, InputTags } from '@polkadot/ui-app';
|
|
import BaseIdentityIcon from '@polkadot/ui-identicon';
|
|
import keyring from '@polkadot/ui-keyring';
|
|
|
|
import AvailableDisplay from './Available';
|
|
import BalanceDisplay from './Balance';
|
|
import BondedDisplay from './Bonded';
|
|
import { classes, getAddrName, getAddrTags, toShortAddress } from './util';
|
|
import CopyButton from './CopyButton';
|
|
import IdentityIcon from './IdentityIcon';
|
|
import NonceDisplay from './Nonce';
|
|
import translate from './translate';
|
|
import UnlockingDisplay from './Unlocking';
|
|
|
|
export type Props = I18nProps & {
|
|
accounts_idAndIndex?: [AccountId?, AccountIndex?],
|
|
balance?: BN | Array<BN>,
|
|
bonded?: BN | Array<BN>,
|
|
buttons?: React.ReactNode,
|
|
children?: React.ReactNode,
|
|
defaultName?: string,
|
|
extraInfo?: React.ReactNode,
|
|
identIconSize?: number,
|
|
isEditable?: boolean,
|
|
isInline?: boolean,
|
|
isShort?: boolean,
|
|
session_validators?: Array<AccountId>,
|
|
value: AccountId | AccountIndex | Address | string | null,
|
|
withAvailable?: boolean,
|
|
withBalance?: boolean,
|
|
withBonded?: boolean,
|
|
withIcon?: boolean,
|
|
withIndex?: boolean,
|
|
withNonce?: boolean,
|
|
withTags?: boolean,
|
|
withUnlocking?: boolean
|
|
};
|
|
|
|
type State = {
|
|
address: string,
|
|
isEditingName: boolean,
|
|
isEditingTags: boolean,
|
|
name: string,
|
|
tags: string[]
|
|
};
|
|
|
|
const DEFAULT_ADDR = '5'.padEnd(16, 'x');
|
|
const ICON_SIZE = 64;
|
|
|
|
class AddressSummary extends React.PureComponent<Props, State> {
|
|
state: State;
|
|
|
|
constructor (props: Props) {
|
|
super(props);
|
|
|
|
this.state = this.createState();
|
|
}
|
|
|
|
static defaultProps = {
|
|
defaultName: '<unknown>'
|
|
};
|
|
|
|
static getDerivedStateFromProps ({ accounts_idAndIndex = [], defaultName, value }: Props, prevState: State): State | null {
|
|
const [_accountId] = accounts_idAndIndex;
|
|
const accountId = _accountId || value;
|
|
const address = accountId
|
|
? accountId.toString()
|
|
: DEFAULT_ADDR;
|
|
const name = getAddrName(address, false, defaultName) || '';
|
|
const tags = getAddrTags(address);
|
|
const state = { tags } as State;
|
|
let hasChanged = false;
|
|
|
|
if (address !== prevState.address) {
|
|
state.address = address;
|
|
hasChanged = true;
|
|
}
|
|
|
|
if (name !== prevState.name) {
|
|
state.name = name;
|
|
hasChanged = true;
|
|
}
|
|
|
|
return hasChanged
|
|
? state
|
|
: null;
|
|
}
|
|
|
|
render () {
|
|
const { accounts_idAndIndex = [], className, isInline, style, withIndex = true } = this.props;
|
|
const [accountId, accountIndex] = accounts_idAndIndex;
|
|
const isValid = accountId || accountIndex;
|
|
|
|
return (
|
|
<div
|
|
className={classes('ui--AddressSummary', !isValid && 'invalid', isInline && 'inline', className)}
|
|
style={style}
|
|
>
|
|
<div className='ui--AddressSummary-base'>
|
|
{this.renderIcon()}
|
|
{this.renderButtons()}
|
|
<div className='ui--AddressSummary-data'>
|
|
{this.renderName()}
|
|
{this.renderAddress()}
|
|
{this.renderAccountIndex(withIndex)}
|
|
</div>
|
|
<div className='ui--AddressSummary-balances'>
|
|
{this.renderAvailable()}
|
|
{this.renderBalance()}
|
|
{this.renderBonded()}
|
|
{this.renderNonce()}
|
|
{this.renderUnlocking()}
|
|
</div>
|
|
{this.renderTags()}
|
|
</div>
|
|
{this.renderChildren()}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
private createState () {
|
|
const { accounts_idAndIndex = [], defaultName, value } = this.props;
|
|
const [_accountId] = accounts_idAndIndex;
|
|
const accountId = _accountId || value;
|
|
const address = accountId
|
|
? accountId.toString()
|
|
: DEFAULT_ADDR;
|
|
const name = getAddrName(address, false, defaultName) || '';
|
|
const tags = getAddrTags(address);
|
|
|
|
return {
|
|
address,
|
|
isEditingName: false,
|
|
isEditingTags: false,
|
|
name,
|
|
tags
|
|
};
|
|
}
|
|
|
|
protected renderAddress () {
|
|
const { isShort = true } = this.props;
|
|
const { address } = this.state;
|
|
const addrElem = isShort
|
|
? (
|
|
<CopyButton
|
|
isAddress
|
|
value={address}
|
|
>
|
|
<span>{toShortAddress(address)}</span>
|
|
</CopyButton>
|
|
)
|
|
: address;
|
|
|
|
return (
|
|
<>
|
|
<div className='ui--AddressSummary-name'>
|
|
{name}
|
|
</div>
|
|
<div className='ui--AddressSummary-accountId'>
|
|
{addrElem}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
protected renderButtons () {
|
|
const { buttons } = this.props;
|
|
|
|
return buttons
|
|
? <div className='ui--AddressSummary-buttons'>{buttons}</div>
|
|
: null;
|
|
}
|
|
|
|
protected renderName () {
|
|
const { isEditable } = this.props;
|
|
const { isEditingName, name } = this.state;
|
|
|
|
return isEditingName
|
|
? (
|
|
<Input
|
|
autoFocus
|
|
defaultValue={name}
|
|
className='full'
|
|
onBlur={this.saveName}
|
|
onChange={this.onChangeName}
|
|
onEnter={this.saveName}
|
|
withLabel={false}
|
|
/>
|
|
)
|
|
: (
|
|
<div
|
|
className={classes('ui--AddressSummary-name', isEditable && 'editable')}
|
|
onClick={isEditable ? this.toggleNameEditor : undefined}
|
|
>
|
|
{name}
|
|
{isEditable && this.renderEditIcon()}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
protected onChangeName = (name: string) => {
|
|
this.setState({ name });
|
|
}
|
|
|
|
protected onChangeTags = (tags: string[]) => {
|
|
this.setState({ tags });
|
|
}
|
|
|
|
protected renderAvailable () {
|
|
const { t, withAvailable } = this.props;
|
|
const { address } = this.state;
|
|
|
|
if (!withAvailable || !address) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<AvailableDisplay
|
|
className='ui--AddressSummary-available'
|
|
label={t('available ')}
|
|
params={address}
|
|
/>
|
|
);
|
|
}
|
|
|
|
protected renderAccountIndex (withIndex: boolean) {
|
|
const { accounts_idAndIndex = [] } = this.props;
|
|
const [, accountIndex] = accounts_idAndIndex;
|
|
|
|
if (!accountIndex || !withIndex) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className='ui--AddressSummary-accountIndex'>
|
|
{accountIndex.toString()}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
protected renderBalance () {
|
|
const { balance, t, withBalance } = this.props;
|
|
const { address } = this.state;
|
|
|
|
if (!withBalance || !address) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<BalanceDisplay
|
|
balance={balance}
|
|
className='ui--AddressSummary-balance'
|
|
label={t('total ')}
|
|
params={address}
|
|
/>
|
|
);
|
|
}
|
|
|
|
protected renderBonded () {
|
|
const { bonded, t, withBonded } = this.props;
|
|
const { address } = this.state;
|
|
|
|
if (!withBonded || !address) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<BondedDisplay
|
|
bonded={bonded}
|
|
className='ui--AddressSummary-bonded'
|
|
label={t('bonded ')}
|
|
params={address}
|
|
/>
|
|
);
|
|
}
|
|
|
|
protected renderChildren () {
|
|
const { children } = this.props;
|
|
|
|
if (!children || (Array.isArray(children) && children.length === 0)) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className='ui--AddressSummary-children'>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
protected renderEditIcon () {
|
|
return (
|
|
<Button
|
|
className='iconButton'
|
|
icon='edit'
|
|
size='mini'
|
|
isPrimary
|
|
key='unlock'
|
|
/>
|
|
);
|
|
}
|
|
|
|
protected renderIcon (className: string = 'ui--AddressSummary-icon', size?: number) {
|
|
const { identIconSize = ICON_SIZE, withIcon = true } = this.props;
|
|
const { address } = this.state;
|
|
|
|
if (!withIcon) {
|
|
return null;
|
|
}
|
|
|
|
// Since we do queries to storage in the wrapped example, we don't want
|
|
// to follow that route if we don't have a valid address.
|
|
const Component = address
|
|
? IdentityIcon
|
|
: BaseIdentityIcon;
|
|
|
|
return (
|
|
<Component
|
|
className={className}
|
|
size={size || identIconSize}
|
|
value={address}
|
|
/>
|
|
);
|
|
}
|
|
|
|
protected renderNonce () {
|
|
const { t, withNonce } = this.props;
|
|
const { address } = this.state;
|
|
|
|
if (!withNonce || !address) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<NonceDisplay
|
|
className='ui--AddressSummary-nonce'
|
|
label={t('transactions ')}
|
|
params={address}
|
|
/>
|
|
);
|
|
}
|
|
|
|
protected renderSaveIcon (callback: () => void) {
|
|
return (
|
|
<Button
|
|
className='saveButton'
|
|
onClick={callback}
|
|
icon='save'
|
|
size='small'
|
|
isPrimary
|
|
key='save'
|
|
/>
|
|
);
|
|
}
|
|
|
|
protected renderTags () {
|
|
const { isEditingTags, tags } = this.state;
|
|
const { isEditable, withTags = false } = this.props;
|
|
|
|
if (!withTags) {
|
|
return null;
|
|
}
|
|
|
|
const resultingDom = isEditingTags ?
|
|
<>
|
|
<InputTags
|
|
className='ui--AddressSummary-tags-input'
|
|
onBlur={this.saveTags}
|
|
onChange={this.onChangeTags}
|
|
onClose={this.saveTags}
|
|
openOnFocus
|
|
defaultValue = {tags}
|
|
searchInput={{ autoFocus: true }}
|
|
value={tags}
|
|
withLabel={false}
|
|
/>
|
|
</>
|
|
:
|
|
<div
|
|
className={classes('ui--AddressSummary-tags', isEditable && 'editable')}
|
|
onClick={isEditable ? this.toggleTagsEditor : undefined}
|
|
>
|
|
{
|
|
!tags.length
|
|
? (isEditable ? <span className='addTags'>add tags</span> : undefined)
|
|
: tags.map((tag) => {
|
|
return (
|
|
<Label key={tag} size='tiny' color='grey'>
|
|
{tag}
|
|
</Label>
|
|
);
|
|
})
|
|
}
|
|
{isEditable && this.renderEditIcon()}
|
|
</div>;
|
|
|
|
return resultingDom;
|
|
}
|
|
|
|
protected renderUnlocking () {
|
|
const { withUnlocking, t } = this.props;
|
|
const { address } = this.state;
|
|
|
|
if (!withUnlocking || !address) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<UnlockingDisplay
|
|
className='ui--AddressSummary-available'
|
|
label={t('unlock ')}
|
|
labelRedeem={t('redeem ')}
|
|
params={address}
|
|
/>
|
|
);
|
|
}
|
|
|
|
protected saveName = () => {
|
|
const { value } = this.props;
|
|
const { address, name } = this.state;
|
|
|
|
const trimmedName = name.trim();
|
|
|
|
// Save only if the name was changed or if it's no empty.
|
|
if (trimmedName && value) {
|
|
const currentKeyring = keyring.getPair(address);
|
|
|
|
currentKeyring && keyring.saveAccountMeta(currentKeyring, {
|
|
name: trimmedName,
|
|
whenEdited: Date.now()
|
|
});
|
|
|
|
this.setState({ isEditingName: false });
|
|
}
|
|
}
|
|
|
|
protected saveTags = () => {
|
|
const { value } = this.props;
|
|
const { address, tags } = this.state;
|
|
|
|
if (value) {
|
|
const currentKeyring = keyring.getPair(address);
|
|
|
|
currentKeyring && keyring.saveAccountMeta(currentKeyring, {
|
|
tags,
|
|
whenEdited: Date.now()
|
|
});
|
|
|
|
this.setState({ isEditingTags: false });
|
|
}
|
|
}
|
|
|
|
protected toggleNameEditor = () => {
|
|
const { value } = this.props;
|
|
|
|
if (value && keyring.getPair(value.toString())) {
|
|
this.setState(({ isEditingName }) => ({
|
|
isEditingName: !isEditingName
|
|
}));
|
|
}
|
|
}
|
|
|
|
protected toggleTagsEditor = () => {
|
|
this.setState(({ isEditingTags }) => ({
|
|
isEditingTags: !isEditingTags
|
|
}));
|
|
}
|
|
}
|
|
|
|
export {
|
|
DEFAULT_ADDR,
|
|
AddressSummary
|
|
};
|
|
|
|
export default translate(
|
|
withCalls<Props>(
|
|
['derive.accounts.idAndIndex', { paramName: 'value' }]
|
|
)(AddressSummary)
|
|
);
|