Swap Addresses to grid layout (as in accounts) (#1224)

* Swap Addresses to grid layout (as in accounts)

* Failsafe in cryptotype

* Edit works. Needs to be made generic.

* Address overview + create dialog
This commit is contained in:
Jaco Greeff 2019-05-28 07:25:49 +02:00 committed by GitHub
parent f3c603ab2e
commit 0eb050c430
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 327 additions and 587 deletions

View File

@ -29,6 +29,7 @@ type State = {
isTransferOpen: boolean
};
// FIXME This is duplicated in app-ddresses
const Wrapper = styled.article`
position: relative;
flex: 1 1;

View File

@ -0,0 +1,183 @@
// Copyright 2017-2019 @polkadot/app-staking 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 { KeyringAddress } from '@polkadot/ui-keyring/types';
import { ActionStatus } from '@polkadot/ui-app/Status/types';
import { I18nProps } from '@polkadot/ui-app/types';
import React from 'react';
import styled from 'styled-components';
import { AddressInfo, AddressRow, Button } from '@polkadot/ui-app';
import keyring from '@polkadot/ui-keyring';
import Forgetting from './modals/Forgetting';
import translate from './translate';
type Props = I18nProps & {
address: string
};
type State = {
current: KeyringAddress,
isEditable: boolean,
isForgetOpen: boolean
};
// FIXME This is duplicated in app-accounts
const Wrapper = styled.article`
position: relative;
flex: 1 1;
min-width: 24%;
max-width: 24%;
justify-content: space-around;
.accounts--Account-buttons {
text-align: right;
margin-bottom: 2em;
}
.ui--AddressSummary-base {
flex: 1;
padding: 0;
}
.accounts--Account-buttons > button {
margin: .2em;
}
@media (max-width: 2060px) {
min-width: 32%;
max-width: 32%;
}
@media (max-width: 1580px) {
min-width: 49%;
max-width: 49%;
}
@media (max-width: 1100px) {
min-width: 100%;
max-width: 100%;
}
`;
class Address extends React.PureComponent<Props> {
state: State;
constructor (props: Props) {
super(props);
const { address } = this.props;
this.state = {
current: keyring.getAddress(address),
isEditable: true,
isForgetOpen: false
};
}
render () {
const { address } = this.props;
const { isEditable } = this.state;
return (
<Wrapper className='overview--Account'>
{this.renderModals()}
<AddressRow
buttons={this.renderButtons()}
isEditable={isEditable}
value={address}
withBalance={false}
withIndex
withNonce={false}
withTags
>
<AddressInfo
withBalance={{ available: true, free: true, total: true }}
withExtended={{ nonce: true }}
value={address}
/>
</AddressRow>
</Wrapper>
);
}
private renderModals () {
const { address } = this.props;
const { isForgetOpen, current } = this.state;
if (!address) {
return null;
}
const modals = [];
if (isForgetOpen) {
modals.push(
<Forgetting
currentAddress={current}
doForget={this.onForget}
key='modal-forget-account'
onClose={this.toggleForget}
/>
);
}
return modals;
}
private toggleForget = (): void => {
const { isForgetOpen } = this.state;
this.setState({
isForgetOpen: !isForgetOpen
});
}
private onForget = (): void => {
const { address, t } = this.props;
if (!address) {
return;
}
const status = {
account: address,
action: 'forget'
} as ActionStatus;
try {
keyring.forgetAddress(address);
status.status = 'success';
status.message = t('address forgotten');
} catch (error) {
status.status = 'error';
status.message = error.message;
}
}
private renderButtons () {
const { t } = this.props;
const { isEditable } = this.state;
return (
<div className='accounts--Account-buttons buttons'>
{isEditable && (
<>
<Button
isNegative
onClick={this.toggleForget}
icon='trash'
size='small'
tooltip={t('Forget this account')}
/>
</>
)}
</div>
);
}
}
export default translate(Address);

View File

@ -1,223 +0,0 @@
// Copyright 2017-2019 @polkadot/app-address-book 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 { I18nProps } from '@polkadot/ui-app/types';
import { ComponentProps } from './types';
import React from 'react';
import { AddressRow, Button, Input, InputTags, TxComponent } from '@polkadot/ui-app';
import { ActionStatus } from '@polkadot/ui-app/Status/types';
import { InputAddress } from '@polkadot/ui-app/InputAddress';
import keyring from '@polkadot/ui-keyring';
import translate from './translate';
type Props = ComponentProps & I18nProps;
type State = {
address: string,
isAddressExisting: boolean,
isAddressValid: boolean,
isNameValid: boolean,
isValid: boolean,
name: string,
tags: Array<string>
};
class Creator extends TxComponent<Props, State> {
state: State;
constructor (props: Props) {
super(props);
this.state = this.emptyState();
}
render () {
const { address, name } = this.state;
return (
<div className='address-book--Creator'>
<div className='ui--grid'>
{this.renderInput()}
<AddressRow
className='shrink'
defaultName={name}
value={address}
/>
</div>
</div>
);
}
renderButtons () {
const { t } = this.props;
const { isValid } = this.state;
return (
<Button.Group>
<Button
onClick={this.onDiscard}
label={t('Reset')}
/>
<Button.Or />
<Button
isDisabled={!isValid}
isPrimary
onClick={this.onCommit}
label={t('Save')}
ref={this.button}
/>
</Button.Group>
);
}
renderInput () {
const { t } = this.props;
const { address, isAddressValid, isNameValid, name, tags } = this.state;
return (
<div className='grow'>
<div className='ui--row'>
<Input
autoFocus
className='full'
help={t('Paste here the address of the contact you want to add to your address book.')}
isError={!isAddressValid}
label={t('address')}
onChange={this.onChangeAddress}
onEnter={this.submit}
value={address}
/>
</div>
<div className='ui--row'>
<Input
className='full'
help={t('Type the name of your contact. This name will be used across all the apps. It can be edited later on.')}
isError={!isNameValid}
label={t('name')}
onChange={this.onChangeName}
onEnter={this.submit}
value={name}
/>
</div>
<div className='ui--row'>
<InputTags
help={t('Additional user-specified tags that can be used to identify the address. Tags can be used for categorization and filtering.')}
label={t('user-defined tags')}
onChange={this.onChangeTags}
value={tags}
/>
</div>
{this.renderButtons()}
</div>
);
}
emptyState (): State {
return {
address: '',
isAddressExisting: false,
isAddressValid: false,
isNameValid: true,
isValid: false,
name: 'new address',
tags: []
};
}
nextState (newState: State, allowEdit: boolean = false): void {
this.setState(
(prevState: State): State => {
let { address = prevState.address, name = prevState.name, tags = prevState.tags } = newState;
let isAddressValid = true;
let isAddressExisting = false;
let newAddress = address;
try {
newAddress = keyring.encodeAddress(
keyring.decodeAddress(address)
);
isAddressValid = keyring.isAvailable(newAddress);
if (!isAddressValid) {
const old = keyring.getAddress(newAddress);
if (old.isValid) {
if (!allowEdit) {
name = old.getMeta().name || name;
}
isAddressExisting = true;
isAddressValid = true;
}
}
} catch (error) {
isAddressValid = false;
}
const isNameValid = !!name;
return {
address: newAddress,
isAddressExisting,
isAddressValid,
isNameValid,
isValid: isAddressValid && isNameValid,
name,
tags
};
}
);
}
onChangeAddress = (address: string): void => {
this.nextState({ address } as State);
}
onChangeName = (name: string): void => {
this.nextState({ name } as State, true);
}
onChangeTags = (tags: Array<string>): void => {
this.setState({ tags });
}
onCommit = (): void => {
const { basePath, onStatusChange, t } = this.props;
const { address, isAddressExisting, name, tags } = this.state;
const status = {
action: 'create'
} as ActionStatus;
try {
keyring.saveAddress(address, { name, tags });
status.account = address;
status.status = address ? 'success' : 'error';
status.message = isAddressExisting
? t('address edited')
: t('address created');
InputAddress.setLastValue('address', address);
} catch (error) {
status.status = 'error';
status.message = error.message;
}
onStatusChange(status);
if (status.status !== 'error') {
window.location.hash = basePath;
}
}
onDiscard = (): void => {
this.setState(this.emptyState());
}
}
export default translate(Creator);

View File

@ -1,284 +0,0 @@
// Copyright 2017-2019 @polkadot/app-address-book 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 { KeyringAddress } from '@polkadot/ui-keyring/types';
import { I18nProps } from '@polkadot/ui-app/types';
import { ComponentProps } from './types';
import React from 'react';
import { AddressRow, Button, Input, InputAddress, InputTags, TxComponent } from '@polkadot/ui-app';
import { ActionStatus } from '@polkadot/ui-app/Status/types';
import keyring from '@polkadot/ui-keyring';
import Forgetting from './Forgetting';
import translate from './translate';
type Props = ComponentProps & I18nProps;
type State = {
current: KeyringAddress | null,
editedName: string,
isEdited: boolean,
isForgetOpen: boolean,
tags: Array<string>
};
class Editor extends TxComponent<Props, State> {
state: State;
constructor (props: Props) {
super(props);
this.state = this.createState(null);
}
render () {
const { isForgetOpen, current } = this.state;
return (
<div className='address-book--Editor'>
<Forgetting
isOpen={isForgetOpen}
onClose={this.toggleForget}
doForget={this.onForget}
currentAddress={current}
/>
{this.renderData()}
</div>
);
}
renderButtons () {
const { t } = this.props;
const { current, isEdited } = this.state;
if (!current) {
return null;
}
return (
<Button.Group>
<Button
isNegative
onClick={this.toggleForget}
label={t('Forget')}
/>
<Button.Group.Divider />
<Button
isDisabled={!isEdited}
onClick={this.onDiscard}
label={t('Reset')}
/>
<Button.Or />
<Button
isDisabled={!isEdited}
isPrimary
onClick={this.onCommit}
label={t('Save')}
ref={this.button}
/>
</Button.Group>
);
}
renderData () {
const { t } = this.props;
const { current, editedName, tags } = this.state;
const address = current
? current.address()
: undefined;
return (
<div className='ui--grid'>
<div className='grow'>
<div className='ui--row'>
<InputAddress
className='full'
help={t('Select the contact you want to edit or forget.')}
hideAddress
isInput={false}
label={t('contact')}
onChange={this.onChangeAddress}
type='address'
value={address}
/>
</div>
<div className='ui--row'>
<Input
className='full'
help={t('Name associated with the selected contact. You can change it here. This name will be changed across all the apps.')}
label={t('name')}
onChange={this.onChangeName}
onEnter={this.submit}
value={editedName}
/>
</div>
<div className='ui--row'>
<InputTags
help={t('Additional user-specified tags that can be used to identify the address. Tags can be used for categorization and filtering.')}
label={t('user-defined tags')}
onChange={this.onChangeTags}
value={tags}
/>
</div>
{this.renderButtons()}
</div>
<AddressRow
className='shrink'
value={address || ''}
/>
</div>
);
}
createState (current: KeyringAddress | null): State {
const meta = current
? current.getMeta()
: {};
return {
current,
editedName: meta.name || '',
isEdited: false,
isForgetOpen: false,
tags: meta.tags || []
};
}
nextState (newState: State = {} as State): void {
this.setState(
(prevState: State): State => {
let { current = prevState.current, editedName = prevState.editedName, tags = prevState.tags } = newState;
const previous = prevState.current || { address: () => null };
let isEdited = false;
if (current && current.isValid()) {
const meta = current.getMeta();
if (current.address() !== previous.address()) {
editedName = meta.name || '';
tags = meta.tags || [];
} else if (editedName !== meta.name) {
isEdited = true;
}
} else {
editedName = '';
}
let isForgetOpen = false;
return {
current,
editedName,
isEdited,
isForgetOpen,
tags
};
}
);
}
onChangeAddress = (accountId: string): void => {
const current = accountId && keyring.decodeAddress(accountId)
? (keyring.getAddress(accountId) || null)
: null;
this.nextState({ current } as State);
}
onChangeName = (editedName: string): void => {
this.nextState({ editedName } as State);
}
onChangeTags = (tags: Array<string>): void => {
this.setState({ isEdited: true, tags });
}
onCommit = (): void => {
const { onStatusChange, t } = this.props;
const { current, editedName, tags } = this.state;
if (!current) {
return;
}
const status = {
account: current.address(),
action: 'edit'
} as ActionStatus;
try {
keyring.saveAddress(current.address(), {
name: editedName,
tags,
whenEdited: Date.now()
});
status.status = current.getMeta().name === editedName ? 'success' : 'error';
status.message = t('address edited');
} catch (error) {
status.status = 'error';
status.message = error.message;
}
onStatusChange(status);
}
onDiscard = (): void => {
const { current } = this.state;
if (!current) {
return;
}
const meta = current.getMeta();
this.nextState({
editedName: meta.name,
tags: meta.tags || []
} as State);
}
toggleForget = (): void => {
this.setState(
({ isForgetOpen }: State) => ({
isForgetOpen: !isForgetOpen
})
);
}
onForget = (): void => {
const { onStatusChange, t } = this.props;
const { current } = this.state;
if (!current) {
return;
}
this.setState(
this.createState(null),
() => {
const status = {
account: current.address(),
action: 'forget'
} as ActionStatus;
try {
keyring.forgetAddress(
current.address()
);
status.status = 'success';
status.message = t('address forgotten');
} catch (error) {
status.status = 'error';
status.message = error.message;
}
onStatusChange(status);
}
);
}
}
export default translate(Editor);

View File

@ -0,0 +1,88 @@
// Copyright 2017-2019 @polkadot/app-address-book 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 { I18nProps } from '@polkadot/ui-app/types';
import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
import { ComponentProps } from './types';
import React from 'react';
import styled from 'styled-components';
import { Button } from '@polkadot/ui-app';
import addressObservable from '@polkadot/ui-keyring/observable/addresses';
import { withMulti, withObservable } from '@polkadot/ui-api';
import CreateModal from './modals/Create';
import Address from './Address';
import translate from './translate';
type Props = ComponentProps & I18nProps & {
addresses?: SubjectInfo[]
};
type State = {
isCreateOpen: boolean
};
const Wrapper = styled.div`
.addresses {
display: flex;
flex-wrap: wrap;
.spacer {
flex: 1 1;
margin: .25rem;
padding: 1rem 1.5rem;
}
}
`;
class Overview extends React.PureComponent<Props, State> {
state: State = {
isCreateOpen: false
};
render () {
const { addresses, onStatusChange, t } = this.props;
const { isCreateOpen } = this.state;
return (
<Wrapper>
<Button.Group>
<Button
isPrimary
label={t('Add contact')}
onClick={this.toggleCreate}
/>
</Button.Group>
{isCreateOpen && (
<CreateModal
onClose={this.toggleCreate}
onStatusChange={onStatusChange}
/>
)}
<div className='addresses'>
{addresses && Object.keys(addresses).map((address) => (
<Address
address={address}
key={address}
/>
))}
<div className='spacer' />
</div>
</Wrapper>
);
}
private toggleCreate = (): void => {
this.setState(({ isCreateOpen }) => ({
isCreateOpen: !isCreateOpen
}));
}
}
export default withMulti(
Overview,
translate,
withObservable(addressObservable.subject, { propName: 'addresses' })
);

View File

@ -16,9 +16,7 @@ import Tabs, { TabItem } from '@polkadot/ui-app/Tabs';
import { withMulti, withObservable } from '@polkadot/ui-api';
import basicMd from './md/basic.md';
import CreateModal from './modals/Create';
import Creator from './Creator';
import Editor from './Editor';
import Overview from './Overview';
import translate from './translate';
type Props = AppProps & I18nProps & {
@ -47,12 +45,8 @@ class AddressBookApp extends React.PureComponent<Props, State> {
isCreateOpen: false,
items: [
{
name: 'edit',
text: t('Edit contact')
},
{
name: 'create',
text: t('Add contact')
name: 'overview',
text: t('Overview')
}
]
};
@ -85,9 +79,8 @@ class AddressBookApp extends React.PureComponent<Props, State> {
}
render () {
const { basePath, onStatusChange } = this.props;
const { hidden, isCreateOpen, items } = this.state;
const renderCreator = this.renderComponent(Creator);
const { basePath } = this.props;
const { hidden, items } = this.state;
return (
<main className='address-book--App'>
@ -100,21 +93,8 @@ class AddressBookApp extends React.PureComponent<Props, State> {
/>
</header>
<Switch>
<Route path={`${basePath}/create`} render={renderCreator} />
<Route
render={
hidden.includes('edit')
? renderCreator
: this.renderComponent(Editor)
}
/>
<Route render={this.renderComponent(Overview)} />
</Switch>
{isCreateOpen && (
<CreateModal
onClose={this.toggleCreate}
onStatusChange={onStatusChange}
/>
)}
</main>
);
}
@ -132,12 +112,6 @@ class AddressBookApp extends React.PureComponent<Props, State> {
);
};
}
private toggleCreate = (): void => {
this.setState(({ isCreateOpen }) => ({
isCreateOpen: !isCreateOpen
}));
}
}
export default withMulti(

View File

@ -8,10 +8,9 @@ import { I18nProps } from '@polkadot/ui-app/types';
import React from 'react';
import { AddressRow, Button, Modal } from '@polkadot/ui-app';
import translate from './translate';
import translate from '../translate';
type Props = I18nProps & {
isOpen: boolean,
onClose: () => void,
doForget: () => void,
currentAddress: KeyringAddress | null
@ -23,11 +22,7 @@ class Forgetting extends React.PureComponent<Props> {
}
render () {
const { isOpen, style } = this.props;
if (!isOpen) {
return null;
}
const { style, t } = this.props;
return (
<Modal
@ -35,6 +30,7 @@ class Forgetting extends React.PureComponent<Props> {
open
style={style}
>
<Modal.Header>{t('Confirm address removal')}</Modal.Header>
{this.renderContent()}
{this.renderButtons()}
</Modal>
@ -71,20 +67,15 @@ class Forgetting extends React.PureComponent<Props> {
: undefined;
return (
<>
<Modal.Header>
{t('Confirm address removal')}
</Modal.Header>
<Modal.Content className='forgetting-Address'>
<AddressRow
isInline
value={address || ''}
>
<p>{t('You are about to remove this address from your address book. Once completed, should you need to access it again, you will have to re-add the address.')}</p>
<p>{t('This operation does not remove the history of the account from the chain, nor any associated funds from the account. The forget operation only limits your access to the address on this browser.')}</p>
</AddressRow>
</Modal.Content>
</>
<Modal.Content className='forgetting-Address'>
<AddressRow
isInline
value={address || ''}
>
<p>{t('You are about to remove this address from your address book. Once completed, should you need to access it again, you will have to re-add the address.')}</p>
<p>{t('This operation does not remove the history of the account from the chain, nor any associated funds from the account. The forget operation only limits your access to the address on this browser.')}</p>
</AddressRow>
</Modal.Content>
);
}
}

View File

@ -87,7 +87,7 @@ class AddressSummary extends React.PureComponent<Props, State> {
hasChanged = true;
}
if (name !== prevState.name) {
if (!prevState.isEditingName && name !== prevState.name) {
state.name = name;
hasChanged = true;
}
@ -427,48 +427,51 @@ class AddressSummary extends React.PureComponent<Props, State> {
}
protected saveName = () => {
const { value } = this.props;
const { address, name } = this.state;
const trimmedName = name.trim();
const meta = {
name: trimmedName,
whenEdited: Date.now()
};
// Save only if the name was changed or if it's no empty.
if (trimmedName && value) {
const currentKeyring = keyring.getPair(address);
if (trimmedName && address) {
try {
const currentKeyring = keyring.getPair(address);
currentKeyring && keyring.saveAccountMeta(currentKeyring, {
name: trimmedName,
whenEdited: Date.now()
});
currentKeyring && keyring.saveAccountMeta(currentKeyring, meta);
} catch (error) {
keyring.saveAddress(address, meta);
}
this.setState({ isEditingName: false });
}
}
protected saveTags = () => {
const { value } = this.props;
const { address, tags } = this.state;
const meta = {
tags,
whenEdited: Date.now()
};
if (value) {
const currentKeyring = keyring.getPair(address);
if (address) {
try {
const currentKeyring = keyring.getPair(address);
currentKeyring && keyring.saveAccountMeta(currentKeyring, {
tags,
whenEdited: Date.now()
});
currentKeyring && keyring.saveAccountMeta(currentKeyring, meta);
} catch (error) {
keyring.saveAddress(address, meta);
}
this.setState({ isEditingTags: false });
}
}
protected toggleNameEditor = () => {
const { value } = this.props;
if (value && keyring.getPair(value.toString())) {
this.setState(({ isEditingName }) => ({
isEditingName: !isEditingName
}));
}
this.setState(({ isEditingName }) => ({
isEditingName: !isEditingName
}));
}
protected toggleTagsEditor = () => {

View File

@ -18,12 +18,19 @@ type Props = BareProps & {
export default class CryptoType extends React.PureComponent<Props> {
render () {
const { accountId, className, label = '' } = this.props;
const current = accountId
let type = '<unknown>';
try {
const current = accountId
? keyring.getPair(accountId.toString())
: null;
const type = current
? current.type
: 'ed25519';
if (current) {
type = current.type;
}
} catch (error) {
// cannot determine, keep unknown
}
return (
<div className={classes('ui--CryptoType', className)}>