Rework address & accounts layout (#2008)

* Display accounts as rows

* Tags display

* Accounts seem ok (as a start)

* Filter by tags

* Don't add new tags via input

* Convert addresses

* Adjustments
This commit is contained in:
Jaco Greeff 2019-12-06 14:01:58 +01:00 committed by GitHub
parent c5a85676d5
commit ee83086c4a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 792 additions and 334 deletions

View File

@ -6,8 +6,9 @@ import { ActionStatus } from '@polkadot/react-components/Status/types';
import { I18nProps } from '@polkadot/react-components/types';
import React, { useState, useEffect } from 'react';
import { Label } from 'semantic-ui-react';
import styled from 'styled-components';
import { AddressCard, AddressInfo, Button, ChainLock, Forget, Menu, Popup } from '@polkadot/react-components';
import { AddressInfo, AddressSmall, Button, ChainLock, Forget, Icon, InputTags, LinkPolkascan, Menu, Popup, Input } from '@polkadot/react-components';
import { useApi } from '@polkadot/react-hooks';
import keyring from '@polkadot/ui-keyring';
@ -19,19 +20,27 @@ import translate from './translate';
interface Props extends I18nProps {
address: string;
allowTags: string[];
className?: string;
isFavorite: boolean;
toggleFavorite: (address: string) => void;
}
function Account ({ address, className, t }: Props): React.ReactElement<Props> {
function Account ({ address, allowTags, className, isFavorite, t, toggleFavorite }: Props): React.ReactElement<Props> | null {
const api = useApi();
const [tags, setTags] = useState<string[]>([]);
const [accName, setAccName] = useState('');
const [genesisHash, setGenesisHash] = useState<string | null>(null);
const [isBackupOpen, setIsBackupOpen] = useState(false);
const [isEditingName, setIsEditingName] = useState(false);
const [isEditingTags, setIsEditingTags] = useState(false);
const [{ isDevelopment, isEditable, isExternal }, setFlags] = useState({ isDevelopment: false, isEditable: false, isExternal: false });
const [isDeriveOpen, setIsDeriveOpen] = useState(false);
const [isForgetOpen, setIsForgetOpen] = useState(false);
const [isPasswordOpen, setIsPasswordOpen] = useState(false);
const [isSettingPopupOpen, setIsSettingPopupOpen] = useState(false);
const [isTransferOpen, setIsTransferOpen] = useState(false);
const [isVisible, setIsVisible] = useState(true);
useEffect((): void => {
const account = keyring.getAccount(address);
@ -42,14 +51,64 @@ function Account ({ address, className, t }: Props): React.ReactElement<Props> {
isEditable: (account && !(account.meta.isInjected || account.meta.isHardware)) || false,
isExternal: (account && account.meta.isExternal) || false
});
setTags(account?.meta?.tags || []);
setAccName(account?.meta?.name || '');
}, [address]);
useEffect((): void => {
if (allowTags.length === 0) {
setIsVisible(true);
} else {
setIsVisible(
allowTags.reduce((result: boolean, tag: string): boolean => {
return result || tags.includes(tag);
}, false)
);
}
}, [allowTags, tags]);
if (!isVisible) {
return null;
}
const _toggleEditName = (): void => setIsEditingName(!isEditingName);
const _toggleEditTags = (): void => setIsEditingTags(!isEditingTags);
const _toggleBackup = (): void => setIsBackupOpen(!isBackupOpen);
const _toggleDerive = (): void => setIsDeriveOpen(!isDeriveOpen);
const _toggleForget = (): void => setIsForgetOpen(!isForgetOpen);
const _togglePass = (): void => setIsPasswordOpen(!isPasswordOpen);
const _toggleSettingPopup = (): void => setIsSettingPopupOpen(!isSettingPopupOpen);
const _toggleTransfer = (): void => setIsTransferOpen(!isTransferOpen);
const _saveName = (): void => {
_toggleEditName();
const meta = { name: accName, whenEdited: Date.now() };
if (address) {
try {
const currentKeyring = keyring.getPair(address);
currentKeyring && keyring.saveAccountMeta(currentKeyring, meta);
} catch (error) {
keyring.saveAddress(address, meta);
}
}
};
const _saveTags = (): void => {
_toggleEditTags();
const meta = { tags, whenEdited: Date.now() };
if (address) {
try {
const currentKeyring = keyring.getPair(address);
currentKeyring && keyring.saveAccountMeta(currentKeyring, meta);
} catch (error) {
keyring.saveAddress(address, meta);
}
}
};
const _onForget = (): void => {
if (!address) {
return;
@ -76,137 +135,193 @@ function Account ({ address, className, t }: Props): React.ReactElement<Props> {
setGenesisHash(genesisHash);
};
const _onFavorite = (): void => toggleFavorite(address);
// FIXME It is a bit heavy-handled switching of being editable here completely
// (and removing the tags, however the keyring cannot save these)
return (
<AddressCard
buttons={
<div className='accounts--Account-buttons buttons'>
<div className='actions'>
<Button
icon='paper plane'
isPrimary
label={t('send')}
onClick={_toggleTransfer}
size='small'
tooltip={t('Send funds from this account')}
/>
<Popup
className='theme--default'
onClose={_toggleSettingPopup}
open={isSettingPopupOpen}
position='bottom right'
trigger={
<Button
icon='setting'
onClick={_toggleSettingPopup}
size='small'
<tr className={className}>
<td className='favorite'>
<Icon
className={`${isFavorite && 'isSelected'}`}
name={isFavorite ? 'star' : 'star outline'}
onClick={_onFavorite}
/>
</td>
<td className='top'>
<AddressSmall
overrideName={
isEditingName
? (
<Input
className='name--input'
autoFocus
defaultValue={accName}
onBlur={_saveName}
onChange={setAccName}
onEnter={_saveName}
withLabel={false}
/>
)
: undefined
}
onClickName={_toggleEditName}
toggle={isEditingName}
value={address}
/>
{isBackupOpen && (
<Backup
address={address}
key='modal-backup-account'
onClose={_toggleBackup}
/>
)}
{isDeriveOpen && (
<Derive
from={address}
key='modal-derive-account'
onClose={_toggleDerive}
/>
)}
{isForgetOpen && (
<Forget
address={address}
onForget={_onForget}
key='modal-forget-account'
onClose={_toggleForget}
/>
)}
{isPasswordOpen && (
<ChangePass
address={address}
key='modal-change-pass'
onClose={_togglePass}
/>
)}
{isTransferOpen && (
<Transfer
key='modal-transfer'
onClose={_toggleTransfer}
senderId={address}
/>
)}
</td>
<td className='top'>
{isEditingTags
? (
<InputTags
onBlur={_saveTags}
onChange={setTags}
onClose={_saveTags}
openOnFocus
defaultValue={tags}
searchInput={{ autoFocus: true }}
value={tags}
withLabel={false}
/>
)
: (
<div className='tags--toggle' onClick={_toggleEditTags}>
{tags.length
? tags.map((tag): React.ReactNode => (
<Label key={tag} size='tiny' color='grey'>{tag}</Label>
))
: <label>{t('no tags')}</label>
}
</div>
)
}
</td>
<td className='top'>
<AddressInfo
address={address}
withBalance
withBalanceToggle
withExtended={false}
/>
</td>
<td className='top'>
<AddressInfo
address={address}
withBalance={false}
withExtended
/>
</td>
<td className='number top'>
<Button
icon='paper plane'
isPrimary
label={t('send')}
onClick={_toggleTransfer}
size='small'
tooltip={t('Send funds from this account')}
/>
<Popup
className='theme--default'
onClose={_toggleSettingPopup}
open={isSettingPopupOpen}
position='bottom right'
trigger={
<Button
icon='setting'
onClick={_toggleSettingPopup}
size='small'
/>
}
>
<Menu
vertical
text
onClick={_toggleSettingPopup}
>
<Menu.Item
disabled={!isEditable || isExternal}
onClick={_toggleDerive}
>
<Menu
vertical
text
onClick={_toggleSettingPopup}
>
<Menu.Item
disabled={!isEditable || isExternal}
onClick={_toggleDerive}
>
{t('Derive account from source')}
</Menu.Item>
<Menu.Item disabled>
{t('Change on-chain nickname')}
</Menu.Item>
<Menu.Item
disabled={!isEditable || isExternal || isDevelopment}
onClick={_toggleBackup}
>
{t('Create a backup file for this account')}
</Menu.Item>
<Menu.Item
disabled={!isEditable || isExternal || isDevelopment}
onClick={_togglePass}
>
{t("Change this account's password")}
</Menu.Item>
<Menu.Item
disabled={!isEditable || isDevelopment}
onClick={_toggleForget}
>
{t('Forget this account')}
</Menu.Item>
{!api.isDevelopment && (
<>
<Menu.Divider />
<ChainLock
className='accounts--network-toggle'
genesisHash={genesisHash}
onChange={_onGenesisChange}
preventDefault
/>
</>
)}
</Menu>
</Popup>
</div>
</div>
}
className={className}
isEditable={isEditable}
type='account'
value={address}
withExplorer
withIndexOrAddress={false}
withTags
>
{address && (
<>
{isBackupOpen && (
<Backup
address={address}
key='modal-backup-account'
onClose={_toggleBackup}
/>
)}
{isDeriveOpen && (
<Derive
from={address}
key='modal-derive-account'
onClose={_toggleDerive}
/>
)}
{isForgetOpen && (
<Forget
address={address}
onForget={_onForget}
key='modal-forget-account'
onClose={_toggleForget}
/>
)}
{isPasswordOpen && (
<ChangePass
address={address}
key='modal-change-pass'
onClose={_togglePass}
/>
)}
{isTransferOpen && (
<Transfer
key='modal-transfer'
onClose={_toggleTransfer}
senderId={address}
/>
)}
</>
)}
<AddressInfo
address={address}
withBalance
withExtended
/>
</AddressCard>
{t('Derive account from source')}
</Menu.Item>
<Menu.Item disabled>
{t('Change on-chain nickname')}
</Menu.Item>
<Menu.Item
disabled={!isEditable || isExternal || isDevelopment}
onClick={_toggleBackup}
>
{t('Create a backup file for this account')}
</Menu.Item>
<Menu.Item
disabled={!isEditable || isExternal || isDevelopment}
onClick={_togglePass}
>
{t("Change this account's password")}
</Menu.Item>
<Menu.Item
disabled={!isEditable || isDevelopment}
onClick={_toggleForget}
>
{t('Forget this account')}
</Menu.Item>
{!api.isDevelopment && (
<>
<Menu.Divider />
<ChainLock
className='accounts--network-toggle'
genesisHash={genesisHash}
onChange={_onGenesisChange}
preventDefault
/>
</>
)}
</Menu>
</Popup>
</td>
<td className='mini top'>
<LinkPolkascan
className='ui--AddressCard-exporer-link'
data={address}
type='address'
withShort
/>
</td>
</tr>
);
}
@ -215,5 +330,19 @@ export default translate(
.accounts--Account-buttons {
text-align: right;
}
.tags--toggle {
cursor: pointer;
width: 100%;
min-height: 1.5rem;
label {
cursor: pointer;
}
}
.name--input {
width: 16rem;
}
`
);

View File

@ -5,11 +5,12 @@
import { I18nProps } from '@polkadot/react-components/types';
import { ComponentProps } from './types';
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import keyring from '@polkadot/ui-keyring';
import { getLedger, isLedger } from '@polkadot/react-api';
import { useAccounts } from '@polkadot/react-hooks';
import { Button, CardGrid } from '@polkadot/react-components';
import { useAccounts, useFavorites } from '@polkadot/react-hooks';
import { Button, InputTags, Table } from '@polkadot/react-components';
import CreateModal from './modals/Create';
import ImportModal from './modals/Import';
@ -21,6 +22,10 @@ import translate from './translate';
interface Props extends ComponentProps, I18nProps {
}
type SortedAccount = { address: string; isFavorite: boolean };
const STORE_FAVS = 'accounts:favorites';
// query the ledger for the address, adding it to the keyring
async function queryLedger (): Promise<void> {
const ledger = getLedger();
@ -34,58 +39,36 @@ async function queryLedger (): Promise<void> {
}
}
function Overview ({ onStatusChange, t }: Props): React.ReactElement<Props> {
function Overview ({ className, onStatusChange, t }: Props): React.ReactElement<Props> {
const { allAccounts, hasAccounts } = useAccounts();
const [isCreateOpen, setIsCreateOpen] = useState(false);
const [isImportOpen, setIsImportOpen] = useState(false);
const [isQrOpen, setIsQrOpen] = useState(false);
const emptyScreen = !(isCreateOpen || isImportOpen || isQrOpen) && !hasAccounts;
const [favorites, toggleFavorite] = useFavorites(STORE_FAVS);
const [sortedAccounts, setSortedAccounts] = useState<SortedAccount[]>([]);
const [tags, setTags] = useState<string[]>([]);
useEffect((): void => {
setSortedAccounts(
allAccounts
.map((address): SortedAccount => ({ address, isFavorite: favorites.includes(address) }))
.sort((a, b): number =>
a.isFavorite === b.isFavorite
? 0
: b.isFavorite
? 1
: -1
)
);
}, [allAccounts, favorites]);
const _toggleCreate = (): void => setIsCreateOpen(!isCreateOpen);
const _toggleImport = (): void => setIsImportOpen(!isImportOpen);
const _toggleQr = (): void => setIsQrOpen(!isQrOpen);
return (
<CardGrid
banner={<Banner />}
buttons={
<Button.Group>
<Button
icon='add'
isPrimary
label={t('Add account')}
onClick={_toggleCreate}
/>
<Button.Or />
<Button
icon='sync'
isPrimary
label={t('Restore JSON')}
onClick={_toggleImport}
/>
<Button.Or />
<Button
icon='qrcode'
isPrimary
label={t('Add via Qr')}
onClick={_toggleQr}
/>
{isLedger() && (
<>
<Button.Or />
<Button
icon='question'
isPrimary
label={t('Query Ledger')}
onClick={queryLedger}
/>
</>
)}
</Button.Group>
}
isEmpty={emptyScreen}
emptyText={t('No account yet?')}
>
<div className={className}>
<Banner />
{isCreateOpen && (
<CreateModal
onClose={_toggleCreate}
@ -104,14 +87,82 @@ function Overview ({ onStatusChange, t }: Props): React.ReactElement<Props> {
onStatusChange={onStatusChange}
/>
)}
{allAccounts.map((address): React.ReactNode => (
<Account
address={address}
key={address}
<Button.Group>
<Button
icon='add'
isPrimary
label={t('Add account')}
onClick={_toggleCreate}
/>
))}
</CardGrid>
<Button.Or />
<Button
icon='sync'
isPrimary
label={t('Restore JSON')}
onClick={_toggleImport}
/>
<Button.Or />
<Button
icon='qrcode'
isPrimary
label={t('Add via Qr')}
onClick={_toggleQr}
/>
{isLedger() && (
<>
<Button.Or />
<Button
icon='question'
isPrimary
label={t('Query Ledger')}
onClick={queryLedger}
/>
</>
)}
</Button.Group>
{hasAccounts
? (
<>
<div className='filter--tags'>
<InputTags
allowAdd={false}
label={t('filter by tags')}
onChange={setTags}
defaultValue={tags}
value={tags}
/>
</div>
<Table>
<Table.Body>
{sortedAccounts.map(({ address, isFavorite }): React.ReactNode => (
<Account
address={address}
allowTags={tags}
isFavorite={isFavorite}
key={address}
toggleFavorite={toggleFavorite}
/>
))}
</Table.Body>
</Table>
</>
)
: t('no accounts yet, create or import and existing')
}
</div>
);
}
export default translate(Overview);
export default translate(
styled(Overview)`
.filter--tags {
.ui--Dropdown {
padding-left: 0;
label {
left: 1.55rem;
}
}
}
`
);

View File

@ -7,8 +7,9 @@ import { ActionStatus } from '@polkadot/react-components/Status/types';
import { I18nProps } from '@polkadot/react-components/types';
import React, { useEffect, useState } from 'react';
import { Label } from 'semantic-ui-react';
import styled from 'styled-components';
import { AddressCard, AddressInfo, Button, ChainLock, Forget, Menu, Popup } from '@polkadot/react-components';
import { AddressSmall, AddressInfo, Button, ChainLock, Icon, InputTags, Input, LinkPolkascan, Forget, Menu, Popup } from '@polkadot/react-components';
import { useApi } from '@polkadot/react-hooks';
import keyring from '@polkadot/ui-keyring';
@ -18,7 +19,10 @@ import translate from './translate';
interface Props extends I18nProps {
address: string;
allowTags: string[];
className?: string;
isFavorite: boolean;
toggleFavorite: (address: string) => void;
}
const WITH_BALANCE = { available: true, bonded: true, free: true, locked: true, reserved: true, total: true };
@ -26,13 +30,18 @@ const WITH_EXTENDED = { nonce: true };
const isEditable = true;
function Address ({ address, className, t }: Props): React.ReactElement<Props> {
function Address ({ address, allowTags, className, isFavorite, t, toggleFavorite }: Props): React.ReactElement<Props> | null {
const api = useApi();
const [tags, setTags] = useState<string[]>([]);
const [accName, setAccName] = useState('');
const [current, setCurrent] = useState<KeyringAddress | null>(null);
const [genesisHash, setGenesisHash] = useState<string | null>(null);
const [isEditingName, setIsEditingName] = useState(false);
const [isEditingTags, setIsEditingTags] = useState(false);
const [isForgetOpen, setIsForgetOpen] = useState(false);
const [isSettingPopupOpen, setIsSettingPopupOpen] = useState(false);
const [isTransferOpen, setIsTransferOpen] = useState(false);
const [isVisible, setIsVisible] = useState(true);
useEffect((): void => {
const current = keyring.getAddress(address);
@ -41,6 +50,31 @@ function Address ({ address, className, t }: Props): React.ReactElement<Props> {
setGenesisHash((current && current.meta.genesisHash) || null);
}, []);
useEffect((): void => {
const account = keyring.getAddress(address);
setTags(account?.meta?.tags || []);
setAccName(account?.meta?.name || '');
}, [address]);
useEffect((): void => {
if (allowTags.length === 0) {
setIsVisible(true);
} else {
setIsVisible(
allowTags.reduce((result: boolean, tag: string): boolean => {
return result || tags.includes(tag);
}, false)
);
}
}, [allowTags, tags]);
if (!isVisible) {
return null;
}
const _toggleEditName = (): void => setIsEditingName(!isEditingName);
const _toggleEditTags = (): void => setIsEditingTags(!isEditingTags);
const _toggleForget = (): void => setIsForgetOpen(!isForgetOpen);
const _toggleSettingPopup = (): void => setIsSettingPopupOpen(!isSettingPopupOpen);
const _toggleTransfer = (): void => setIsTransferOpen(!isTransferOpen);
@ -70,96 +104,188 @@ function Address ({ address, className, t }: Props): React.ReactElement<Props> {
setGenesisHash(genesisHash);
};
const _onFavorite = (): void => toggleFavorite(address);
const _saveName = (): void => {
_toggleEditName();
const meta = { name: accName, whenEdited: Date.now() };
if (address) {
try {
const currentKeyring = keyring.getPair(address);
currentKeyring && keyring.saveAccountMeta(currentKeyring, meta);
} catch (error) {
keyring.saveAddress(address, meta);
}
}
};
const _saveTags = (): void => {
_toggleEditTags();
const meta = { tags, whenEdited: Date.now() };
if (address) {
try {
const currentKeyring = keyring.getPair(address);
currentKeyring && keyring.saveAccountMeta(currentKeyring, meta);
} catch (error) {
keyring.saveAddress(address, meta);
}
}
};
return (
<AddressCard
buttons={
<div className='addresses--Address-buttons buttons'>
<div className='actions'>
<Button
icon='paper plane'
isPrimary
key='deposit'
label={t('deposit')}
onClick={_toggleTransfer}
size='small'
tooltip={t('Send funds to this address')}
/>
<Popup
className='theme--default'
onClose={_toggleSettingPopup}
open={isSettingPopupOpen}
position='bottom right'
trigger={
<Button
icon='setting'
onClick={_toggleSettingPopup}
size='small'
<tr className={className}>
<td className='favorite'>
<Icon
className={`${isFavorite && 'isSelected'}`}
name={isFavorite ? 'star' : 'star outline'}
onClick={_onFavorite}
/>
</td>
<td className='top'>
<AddressSmall
overrideName={
isEditingName
? (
<Input
className='name--input'
autoFocus
defaultValue={accName}
onBlur={_saveName}
onChange={setAccName}
onEnter={_saveName}
withLabel={false}
/>
)
: undefined
}
onClickName={_toggleEditName}
toggle={isEditingName}
value={address}
/>
{address && current && (
<>
{isForgetOpen && (
<Forget
address={current.address}
onForget={_onForget}
key='modal-forget-account'
mode='address'
onClose={_toggleForget}
/>
)}
{isTransferOpen && (
<Transfer
key='modal-transfer'
onClose={_toggleTransfer}
recipientId={address}
/>
)}
</>
)}
</td>
<td className='top'>
{isEditingTags
? (
<InputTags
onBlur={_saveTags}
onChange={setTags}
onClose={_saveTags}
openOnFocus
defaultValue={tags}
searchInput={{ autoFocus: true }}
value={tags}
withLabel={false}
/>
)
: (
<div className='tags--toggle' onClick={_toggleEditTags}>
{tags.length
? tags.map((tag): React.ReactNode => (
<Label key={tag} size='tiny' color='grey'>{tag}</Label>
))
: <label>{t('no tags')}</label>
}
</div>
)
}
</td>
<td className='top'>
<AddressInfo
address={address}
withBalance={WITH_BALANCE}
withBalanceToggle
withExtended={false}
/>
</td>
<td className='top'>
<AddressInfo
address={address}
withBalance={false}
withExtended={WITH_EXTENDED}
/>
</td>
<td className='number top'>
<Button
icon='paper plane'
isPrimary
key='deposit'
label={t('deposit')}
onClick={_toggleTransfer}
size='small'
tooltip={t('Send funds to this address')}
/>
<Popup
className='theme--default'
onClose={_toggleSettingPopup}
open={isSettingPopupOpen}
position='bottom right'
trigger={
<Button
icon='setting'
onClick={_toggleSettingPopup}
size='small'
/>
}
>
<Menu
vertical
text
onClick={_toggleSettingPopup}
>
<Menu.Item
disabled={!isEditable}
onClick={_toggleForget}
>
<Menu
vertical
text
onClick={_toggleSettingPopup}
>
<Menu.Item
disabled={!isEditable}
onClick={_toggleForget}
>
{t('Forget this address')}
</Menu.Item>
{!api.isDevelopment && (
<>
<Menu.Divider />
<ChainLock
className='addresses--network-toggle'
genesisHash={genesisHash}
isDisabled={!isEditable}
onChange={_onGenesisChange}
preventDefault
/>
</>
)}
</Menu>
</Popup>
</div>
</div>
}
className={className}
isEditable={isEditable}
type='address'
value={address}
withExplorer
withIndexOrAddress={false}
withTags
>
{address && current && (
<>
{isForgetOpen && (
<Forget
address={current.address}
onForget={_onForget}
key='modal-forget-account'
mode='address'
onClose={_toggleForget}
/>
)}
{isTransferOpen && (
<Transfer
key='modal-transfer'
onClose={_toggleTransfer}
recipientId={address}
/>
)}
</>
)}
<AddressInfo
address={address}
withBalance={WITH_BALANCE}
withExtended={WITH_EXTENDED}
/>
</AddressCard>
{t('Forget this address')}
</Menu.Item>
{!api.isDevelopment && (
<>
<Menu.Divider />
<ChainLock
className='addresses--network-toggle'
genesisHash={genesisHash}
isDisabled={!isEditable}
onChange={_onGenesisChange}
preventDefault
/>
</>
)}
</Menu>
</Popup>
</td>
<td className='mini top'>
<LinkPolkascan
className='ui--AddressCard-exporer-link'
data={address}
type='address'
withShort
/>
</td>
</tr>
);
}
@ -168,5 +294,19 @@ export default translate(
.addresses--Address-buttons {
text-align: right;
}
.tags--toggle {
cursor: pointer;
width: 100%;
min-height: 1.5rem;
label {
cursor: pointer;
}
}
.name--input {
width: 16rem;
}
`
);

View File

@ -5,9 +5,10 @@
import { I18nProps } from '@polkadot/react-components/types';
import { ComponentProps } from './types';
import React, { useState } from 'react';
import { Button, CardGrid } from '@polkadot/react-components';
import { useAddresses } from '@polkadot/react-hooks';
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import { Button, InputTags, Table } from '@polkadot/react-components';
import { useAddresses, useFavorites } from '@polkadot/react-hooks';
import CreateModal from './modals/Create';
import Address from './Address';
@ -16,42 +17,92 @@ import translate from './translate';
interface Props extends ComponentProps, I18nProps {
}
function Overview ({ onStatusChange, t }: Props): React.ReactElement<Props> {
type SortedAddress = { address: string; isFavorite: boolean };
const STORE_FAVS = 'accounts:favorites';
function Overview ({ className, onStatusChange, t }: Props): React.ReactElement<Props> {
const { hasAddresses, allAddresses } = useAddresses();
const [isCreateOpen, setIsCreateOpen] = useState(false);
const emptyScreen = !isCreateOpen && !hasAddresses;
const [favorites, toggleFavorite] = useFavorites(STORE_FAVS);
const [sortedAddresses, setSortedAddresses] = useState<SortedAddress[]>([]);
const [tags, setTags] = useState<string[]>([]);
useEffect((): void => {
setSortedAddresses(
allAddresses
.map((address): SortedAddress => ({ address, isFavorite: favorites.includes(address) }))
.sort((a, b): number =>
a.isFavorite === b.isFavorite
? 0
: b.isFavorite
? 1
: -1
)
);
}, [allAddresses, favorites]);
const _toggleCreate = (): void => setIsCreateOpen(!isCreateOpen);
return (
<CardGrid
buttons={
<Button.Group>
<Button
icon='add'
isPrimary
label={t('Add contact')}
onClick={_toggleCreate}
/>
</Button.Group>
}
isEmpty={emptyScreen}
emptyText={t('No contacts found.')}
>
<div className={className}>
<Button.Group>
<Button
icon='add'
isPrimary
label={t('Add contact')}
onClick={_toggleCreate}
/>
</Button.Group>
{isCreateOpen && (
<CreateModal
onClose={_toggleCreate}
onStatusChange={onStatusChange}
/>
)}
{allAddresses.map((address): React.ReactNode => (
<Address
address={address}
key={address}
/>
))}
</CardGrid>
{hasAddresses
? (
<>
<div className='filter--tags'>
<InputTags
allowAdd={false}
label={t('filter by tags')}
onChange={setTags}
defaultValue={tags}
value={tags}
/>
</div>
<Table>
<Table.Body>
{sortedAddresses.map(({ address, isFavorite }): React.ReactNode => (
<Address
address={address}
allowTags={tags}
isFavorite={isFavorite}
key={address}
toggleFavorite={toggleFavorite}
/>
))}
</Table.Body>
</Table>
</>
)
: t('no addresses yet add and existring contact')
}
</div>
);
}
export default translate(Overview);
export default translate(
styled(Overview)`
.filter--tags {
.ui--Dropdown {
padding-left: 0;
label {
left: 1.55rem;
}
}
}
`
);

View File

@ -51,6 +51,7 @@ interface Props extends BareProps, I18nProps {
extraInfo?: [string, string][];
stakingInfo?: DerivedStaking;
withBalance?: boolean | BalanceActiveType;
withBalanceToggle?: false;
withExtended?: boolean | CryptoActiveType;
withHexSessionId?: (string | null)[];
withRewardDestination?: boolean;
@ -237,7 +238,7 @@ function renderValidatorPrefs ({ stakingInfo, t, withValidatorPrefs = false }: P
}
function renderBalances (props: Props, allAccounts: string[]): React.ReactNode {
const { balancesAll, stakingInfo, t, withBalance = true } = props;
const { balancesAll, stakingInfo, t, withBalance = true, withBalanceToggle = false } = props;
const balanceDisplay = withBalance === true
? DEFAULT_BALANCES
: withBalance || false;
@ -249,7 +250,7 @@ function renderBalances (props: Props, allAccounts: string[]): React.ReactNode {
const [ownBonded, otherBonded] = calcBonded(stakingInfo, balanceDisplay.bonded);
const controllerId = stakingInfo?.controllerId?.toString();
return (
const allItems = (
<>
{balancesAll && balanceDisplay.total && (
<>
@ -344,15 +345,39 @@ function renderBalances (props: Props, allAccounts: string[]): React.ReactNode {
)}
</>
);
if (withBalanceToggle) {
return (
<>
<label>{t('balances')}</label>
<details>
<summary>
<div className='body'>
<FormatBalance value={balancesAll?.votingBalance} />
</div>
</summary>
<div className='body column'>
{allItems}
</div>
</details>
</>
);
}
return (
<>
{allItems}
</>
);
}
function AddressInfo (props: Props): React.ReactElement<Props> {
const { allAccounts } = useAccounts();
const { className, children, extraInfo, stakingInfo, t, withHexSessionId, withRewardDestination } = props;
const { className, children, extraInfo, stakingInfo, t, withBalanceToggle, withHexSessionId, withRewardDestination } = props;
return (
<div className={className}>
<div className='column'>
<div className={`ui--AddressInfo ${className} ${withBalanceToggle ? 'ui--AddressInfo-expander' : ''}`}>
<div className={`column ${withBalanceToggle ? 'column--expander' : ''}`}>
{renderBalances(props, allAccounts)}
{withHexSessionId && withHexSessionId[0] && (
<>
@ -402,36 +427,62 @@ export default withMulti(
align-items: flex-start;
display: flex;
flex: 1;
justify-content: center;
white-space: nowrap;
&:not(.ui--AddressInfo-expander) {
justify-content: center;
}
.column {
flex: 1;
display: grid;
opacity: 1;
&.column--expander {
text-align: left;
width: 15rem;
label {
grid-column: 1;
padding-right: 0.5rem;
text-align: right;
vertical-align: middle;
details[open] summary {
.body {
opacity: 0;
}
}
.help.circle.icon {
display: none;
details summary {
width: 100%;
.body {
display: inline-block;
text-align: right;
min-width: 12rem;
}
}
}
.result {
grid-column: 2;
&:not(.column--expander) {
flex: 1;
display: grid;
opacity: 1;
.icon {
margin-left: .3em;
margin-right: 0;
padding-right: 0 !important;
label {
grid-column: 1;
padding-right: 0.5rem;
text-align: right;
vertical-align: middle;
.help.circle.icon {
display: none;
}
}
button.ui.icon.primary.button.icon-button {
background: white !important;
.result {
grid-column: 2;
.icon {
margin-left: .3em;
margin-right: 0;
padding-right: 0 !important;
}
button.ui.icon.primary.button.icon-button {
background: white !important;
}
}
}
}

View File

@ -13,10 +13,13 @@ import IdentityIcon from './IdentityIcon';
interface Props {
className?: string;
defaultName?: string;
onClickName?: () => void;
overrideName?: React.ReactNode;
toggle?: any;
value?: string | Address | AccountId;
}
function AddressSmall ({ className, defaultName, value }: Props): React.ReactElement<Props> {
function AddressSmall ({ className, defaultName, onClickName, overrideName, toggle, value }: Props): React.ReactElement<Props> {
return (
<div className={`ui--AddressSmall ${className}`}>
<IdentityIcon
@ -24,7 +27,14 @@ function AddressSmall ({ className, defaultName, value }: Props): React.ReactEle
value={value}
/>
<div className='nameInfo'>
<AccountName defaultName={defaultName} params={value} />
<AccountName
className={(overrideName || !onClickName) ? '' : 'name--clickable'}
defaultName={defaultName}
override={overrideName}
onClick={onClickName}
params={value}
toggle={toggle}
/>
<AccountIndex params={value} />
</div>
</div>
@ -35,6 +45,10 @@ export default styled(AddressSmall)`
vertical-align: middle;
white-space: nowrap;
.name--clickable {
cursor: pointer;
}
.ui--IdentityIcon,
.nameInfo {
display: inline-block;
@ -45,9 +59,11 @@ export default styled(AddressSmall)`
margin-right: 0.75rem;
}
.nameInfo > div {
max-width: 16rem;
overflow: hidden;
text-overflow: ellipsis;
.nameInfo {
> div {
max-width: 16rem;
overflow: hidden;
text-overflow: ellipsis;
}
}
`;

View File

@ -54,10 +54,10 @@ function onAddTag (value: string): void {
saveTags(tags);
}
export default function InputTags ({ className, defaultValue, help, isDisabled, isError, label, onBlur, onChange, onClose, placeholder, searchInput, value, withLabel }: Props): React.ReactElement<Props> {
export default function InputTags ({ allowAdd = true, className, defaultValue, help, isDisabled, isError, label, onBlur, onChange, onClose, placeholder, searchInput, value, withLabel }: Props): React.ReactElement<Props> {
return (
<Dropdown
allowAdd={!isDisabled}
allowAdd={allowAdd && !isDisabled}
className={className}
defaultValue={defaultValue}
help={help}

View File

@ -9,6 +9,7 @@ import styled from 'styled-components';
import { useApi } from '@polkadot/react-hooks';
import translate from './translate';
import Icon from './Icon';
export type LinkTypes = 'address' | 'block' | 'extrinsic';
@ -16,6 +17,7 @@ interface Props extends I18nProps {
className?: string;
data: string;
type: LinkTypes;
withShort?: boolean;
}
const BASE = 'https://polkascan.io/pre/';
@ -34,7 +36,7 @@ const TYPES: Record<string, string> = {
extrinsic: '/system/extrinsic/'
};
function LinkPolkascan ({ className, data, t, type }: Props): React.ReactElement<Props> | null {
function LinkPolkascan ({ className, data, t, type, withShort }: Props): React.ReactElement<Props> | null {
const { systemChain } = useApi();
const extChain = CHAINS[systemChain];
const extType = TYPES[type];
@ -44,13 +46,16 @@ function LinkPolkascan ({ className, data, t, type }: Props): React.ReactElement
}
return (
<div className={className}>
<div className={`${className} ${withShort ? 'withShort' : ''}`}>
<a
href={`${BASE}${extChain}${extType}${data}`}
rel='noopener noreferrer'
target='_blank'
>
{t('View this {{type}} on Polkascan.io', { replace: { type } })}
{withShort
? <Icon name='external' />
: t('View this {{type}} on Polkascan.io', { replace: { type } })
}
</a>
</div>
);

View File

@ -343,7 +343,7 @@ export default class Row<P extends RowProps, S extends RowState> extends React.P
onChange={this.onChangeTags}
onClose={this.saveTags}
openOnFocus
defaultValue = {tags}
defaultValue={tags}
searchInput={{ autoFocus: true }}
value={tags}
withLabel={false}

View File

@ -57,7 +57,6 @@ export default styled(Table)`
margin-bottom: 1.5rem;
table {
/* border-collapse: collapse; */
border-collapse: separate;
border-spacing: 0 0.25rem;
width: 100%;
@ -79,6 +78,10 @@ export default styled(Table)`
label {
opacity: 1;
}
td {
border-color: #ccc;
}
}
&:not(:hover) {
@ -146,6 +149,10 @@ export default styled(Table)`
cursor: pointer;
}
&.mini {
padding: 0 0.75rem 0 0;
}
&:first-child {
border-left-width: 1px;
border-radius: 0.25rem 0 0 0.25rem;

View File

@ -14,7 +14,10 @@ interface Props extends BareProps {
children?: React.ReactNode;
defaultName?: string;
label?: React.ReactNode;
onClick?: () => void;
override?: React.ReactNode;
params?: AccountId | AccountIndex | Address | string | null;
toggle?: any;
withShort?: boolean;
}
@ -40,7 +43,7 @@ function defaultOrAddr (defaultName = '', _address?: AccountId | AccountIndex |
return extracted;
}
export default function AccountName ({ children, className, defaultName, label = '', params, style, withShort }: Props): React.ReactElement<Props> {
export default function AccountName ({ children, className, defaultName, label = '', onClick, override, params, style, toggle, withShort }: Props): React.ReactElement<Props> {
const { api } = useApi();
const info = useStream<DeriveAccountInfo>(api.derive.accounts.info as any, [params]);
const [name, setName] = useState(defaultOrAddr(defaultName, params));
@ -56,14 +59,19 @@ export default function AccountName ({ children, className, defaultName, label =
} else {
setName(defaultOrAddr(defaultName, accountId || params, withShort ? null : accountIndex));
}
}, [info]);
}, [info, toggle]);
return (
<div
className={className}
onClick={
override
? undefined
: onClick
}
style={style}
>
{label}{name}{children}
{label}{override || name}{children}
</div>
);
}