Small init changes (static state) (#663)

* Small init changes (static state)

* Hide vanity on empty accounts

* Require accounts for apps, cleanup needsApi/needsCalls

* Display InputAddress when disable (but no options)

* Filter toolbox based on accounts

* Accounts gated via app, don't display add

* Cleanup linting

* Validation display & clear on settings types

* Split save for developer & general

* Add Reset button to dev settings
This commit is contained in:
Jaco Greeff 2019-01-31 18:02:47 +01:00 committed by GitHub
parent 15cc5190f0
commit a8ed300dc4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 446 additions and 474 deletions

View File

@ -27,7 +27,7 @@ export default class Transfer extends React.PureComponent<Props> {
<section>
<h1>transfer</h1>
<div className='ui--row'>
<article className='medium'>
<div className='large'>
<InputAddress
label='recipient address for this transfer'
onChange={this.onChangeRecipient}
@ -45,7 +45,7 @@ export default class Transfer extends React.PureComponent<Props> {
tx='balances.transfer'
/>
</Button.Group>
</article>
</div>
<div className='template--summary small'>Make a transfer from any account you control to another account. Transfer fees and per-transaction fees apply and will be calculated upon submission.</div>
</div>
</section>

View File

@ -25,16 +25,10 @@ type State = {
};
class Backup extends React.PureComponent<Props, State> {
state: State;
constructor (props: Props) {
super(props);
this.state = {
isPassValid: false,
password: ''
};
}
state: State = {
isPassValid: false,
password: ''
};
render () {
return (

View File

@ -26,18 +26,12 @@ type State = {
};
class ChangePass extends React.PureComponent<Props, State> {
state: State;
constructor (props: Props) {
super(props);
this.state = {
isNewValid: false,
isOldValid: false,
newPass: '',
oldPass: ''
};
}
state: State = {
isNewValid: false,
isOldValid: false,
newPass: '',
oldPass: ''
};
render () {
return (

View File

@ -25,18 +25,12 @@ type State = {
};
class Restore extends React.PureComponent<Props, State> {
state: State;
constructor (props: Props) {
super(props);
this.state = {
isFileValid: false,
isPassValid: false,
json: null,
password: ''
};
}
state: State = {
isFileValid: false,
isPassValid: false,
json: null,
password: ''
};
render () {
const { t } = this.props;

View File

@ -27,7 +27,7 @@ type Props = AppProps & I18nProps & {
type State = {
hidden: Array<string>,
items: Array<TabItem>
tabs: Array<TabItem>
};
class AccountsApp extends React.PureComponent<Props, State> {
@ -43,7 +43,7 @@ class AccountsApp extends React.PureComponent<Props, State> {
this.state = {
...baseState,
items: [
tabs: [
{
name: 'edit',
text: t('Edit account')
@ -72,8 +72,10 @@ class AccountsApp extends React.PureComponent<Props, State> {
}
static hideEditState () {
// Hide vanity as well - since the route order and matching changes, the
// /create/:seed route become problematic, so don't allow that option
return {
hidden: ['edit']
hidden: ['edit', 'vanity']
};
}
@ -93,7 +95,7 @@ class AccountsApp extends React.PureComponent<Props, State> {
render () {
const { basePath } = this.props;
const { hidden, items } = this.state;
const { hidden, tabs } = this.state;
const renderCreator = this.renderComponent(Creator);
return (
@ -102,7 +104,7 @@ class AccountsApp extends React.PureComponent<Props, State> {
<Tabs
basePath={basePath}
hidden={hidden}
items={items}
items={tabs}
/>
</header>
<Switch>
@ -122,13 +124,14 @@ class AccountsApp extends React.PureComponent<Props, State> {
);
}
private renderComponent = (Component: React.ComponentType<ComponentProps>) => {
private renderComponent (Component: React.ComponentType<ComponentProps>) {
return ({ match }: LocationProps) => {
const { basePath, onStatusChange } = this.props;
const { basePath, location, onStatusChange } = this.props;
return (
<Component
basePath={basePath}
location={location}
match={match}
onStatusChange={onStatusChange}
/>

View File

@ -107,13 +107,14 @@ class AddressesApp extends React.PureComponent<Props, State> {
);
}
private renderComponent = (Component: React.ComponentType<ComponentProps>) => {
private renderComponent (Component: React.ComponentType<ComponentProps>) {
return () => {
const { basePath, onStatusChange } = this.props;
const { basePath, location, onStatusChange } = this.props;
return (
<Component
basePath={basePath}
location={location}
onStatusChange={onStatusChange}
/>
);

View File

@ -44,20 +44,14 @@ type State = {
};
class Referendum extends React.PureComponent<Props, State> {
state: State;
constructor (props: Props) {
super(props);
this.state = {
voteCount: 0,
voteCountYay: 0,
voteCountNay: 0,
votedTotal: new BN(0),
votedYay: new BN(0),
votedNay: new BN(0)
};
}
state: State = {
voteCount: 0,
voteCountYay: 0,
voteCountNay: 0,
votedTotal: new BN(0),
votedYay: new BN(0),
votedNay: new BN(0)
};
static getDerivedStateFromProps ({ democracy_referendumVotesFor }: Props, prevState: State): State | null {
if (!democracy_referendumVotesFor) {

View File

@ -24,14 +24,10 @@ type State = {
};
class EventsRecent extends React.PureComponent<Props, State> {
constructor (props: Props) {
super(props);
this.state = {
prevEventHash: '',
recentEvents: []
};
}
state: State = {
prevEventHash: '',
recentEvents: []
};
static getDerivedStateFromProps ({ system_events = [] }: Props, prevState: State): State | null {
const prevEventHash = xxhashAsHex(stringToU8a(JSON.stringify(system_events)));

View File

@ -8,33 +8,17 @@ import { AppProps, I18nProps } from '@polkadot/ui-app/types';
import './index.css';
import React from 'react';
import { Link } from 'react-router-dom';
import { QueueConsumer } from '@polkadot/ui-app/Status/Context';
import { Tabs } from '@polkadot/ui-app/index';
import accountsObservable from '@polkadot/ui-keyring/observable/accounts';
import { withMulti, withObservable } from '@polkadot/ui-api/index';
import Selection from './Selection';
import translate from './translate';
type Props = AppProps & I18nProps & {
accountAll?: Array<any>
};
type Props = AppProps & I18nProps;
class ExtrinsicsApp extends React.PureComponent<Props> {
render () {
const { accountAll, basePath, t } = this.props;
if (!accountAll || !Object.keys(accountAll).length) {
return (
<main className='extrinsics--App'>
{t('There are no saved accounts. ')}
<Link to='/accounts'>
{t('Add Accounts.')}
</Link>
</main>
);
}
const { basePath, t } = this.props;
return (
<main className='extrinsics--App'>
@ -59,8 +43,4 @@ class ExtrinsicsApp extends React.PureComponent<Props> {
export { ExtrinsicsApp };
export default withMulti(
ExtrinsicsApp,
translate,
withObservable(accountsObservable.subject, { propName: 'accountAll' })
);
export default translate(ExtrinsicsApp);

View File

@ -3,7 +3,7 @@
/* of the Apache-2.0 license. See the LICENSE file for details. */
.sub-label {
cursor: pointer;
padding: 0rem .5833rem;
font-size: .85rem;
text-align: right;
}

View File

@ -3,12 +3,14 @@
// of the Apache-2.0 license. See the LICENSE file for details.
import { AppProps, I18nProps } from '@polkadot/ui-app/types';
import { TabItem } from '@polkadot/ui-app/Tabs';
import { SettingsStruct } from '@polkadot/ui-settings/types';
import React from 'react';
import { Route, Switch } from 'react-router';
import store from 'store';
import typeRegistry from '@polkadot/types/codec/typeRegistry';
import { Button, Dropdown, Input, InputFile } from '@polkadot/ui-app/index';
import { Button, Dropdown, Input, InputFile, Tabs } from '@polkadot/ui-app/index';
import uiSettings from '@polkadot/ui-settings';
import { u8aToString } from '@polkadot/util';
@ -19,130 +21,193 @@ import translate from './translate';
type Props = AppProps & I18nProps;
type State = {
settings: SettingsStruct & {
types?: { [index: string]: any } | null,
typesError?: boolean,
typesPlaceholder?: string,
customNode: boolean
}
isCustomNode: boolean,
isTypesValid: boolean,
isUrlValid: boolean,
settings: SettingsStruct,
tabs: Array<TabItem>,
types?: { [index: string]: any } | null,
typesPlaceholder?: string
};
class App extends React.PureComponent<Props, State> {
constructor (props: Props) {
super(props);
const { t } = props;
const types = store.get('types') || {};
const names = Object.keys(types);
const presets = uiSettings.get();
const settings = uiSettings.get();
let isCustomNode = true;
// check to see if user has saved a custom node by seeing if their URL is equal to any preset
let customNode = true;
for (let i = 0; i < uiSettings.availableNodes.length; i++) {
if (uiSettings.availableNodes[i].value === presets.apiUrl) {
customNode = false;
if (uiSettings.availableNodes[i].value === settings.apiUrl) {
isCustomNode = false;
}
}
this.state = {
settings: {
...presets,
typesPlaceholder: names.length
? names.join(', ')
: undefined,
customNode: customNode
}
isCustomNode,
isTypesValid: true,
isUrlValid: this.isValidUrl(settings.apiUrl),
tabs: [
{
name: 'general',
text: t('General')
},
{
name: 'developer',
text: t('Developer')
}
],
typesPlaceholder: names.length
? names.join(', ')
: undefined,
settings
};
}
render () {
const { t } = this.props;
const { settings: { apiUrl, i18nLang, typesPlaceholder, typesError, uiMode, uiTheme, customNode } } = this.state;
const { basePath } = this.props;
const { tabs } = this.state;
return (
<main className='settings--App'>
<section>
<h1>{t('general')}</h1>
<div className='ui--row'>
<div className='full'>
<div className='sub-label'>
{
customNode
? <><a onClick={this.toggleCustomNode}>{t('pre-set')}</a> | <b>{t('custom')}</b></>
: <><b>{t('pre-set')}</b> | <a onClick={this.toggleCustomNode}>{t('custom')}</a></>
}
</div>
{
customNode
? <Input
defaultValue={apiUrl}
label={t('remote node/endpoint to connect to')}
onChange={this.onChangeApiUrl}
/>
: <Dropdown
defaultValue={apiUrl}
label={t('remote node/endpoint to connect to')}
onChange={this.onChangeApiUrl}
options={uiSettings.availableNodes}
/>
}
</div>
</div>
<div className='ui--row'>
<div className='medium'>
<Dropdown
defaultValue={uiTheme}
label={t('default interface theme')}
onChange={this.onChangeUiTheme}
options={uiSettings.availableUIThemes}
/>
</div>
<div className='medium'>
<Dropdown
defaultValue={uiMode}
label={t('interface operation mode')}
onChange={this.onChangeUiMode}
options={uiSettings.availableUIModes}
/>
</div>
</div>
<div className='ui--row'>
<div className='full'>
<Dropdown
defaultValue={i18nLang}
isDisabled
label={t('default interface language')}
onChange={this.onChangeLang}
options={uiSettings.availableLanguages}
/>
</div>
</div>
</section>
<section>
<h1>{t('developer')}</h1>
<div className='ui--row'>
<div className='full'>
<InputFile
isError={typesError}
label={t('additional type definitions (JSON)')}
onChange={this.onChangeTypes}
placeholder={typesPlaceholder}
/>
</div>
</div>
</section>
<Button.Group>
<Button
isPrimary
onClick={this.save}
label={t('Save & Reload')}
<header>
<Tabs
basePath={basePath}
items={tabs}
/>
</Button.Group>
</header>
<Switch>
<Route path={`${basePath}/developer`} render={this.renderDeveloper} />
<Route render={this.renderGeneral} />
</Switch>
</main>
);
}
private renderDeveloper = () => {
const { t } = this.props;
const { isTypesValid, types, typesPlaceholder } = this.state;
return (
<>
<div className='ui--row'>
<div className='full'>
<InputFile
clearContent={!types && isTypesValid}
isError={!isTypesValid}
label={t('additional type definitions (JSON)')}
onChange={this.onChangeTypes}
placeholder={typesPlaceholder}
/>
</div>
</div>
<Button.Group>
<Button
isDisabled={!types}
isNegative
onClick={this.clearTypes}
label={t('Reset')}
/>
<Button.Or />
<Button
isDisabled={!isTypesValid}
isPrimary
onClick={this.saveDeveloper}
label={t('Save')}
/>
</Button.Group>
</>
);
}
private renderGeneral = () => {
const { t } = this.props;
const { isCustomNode, isUrlValid, settings: { apiUrl, i18nLang, uiMode, uiTheme } } = this.state;
return (
<>
<div className='ui--row'>
<div className='full'>
<div className='sub-label'>
{
isCustomNode
? <><a onClick={this.toggleCustomNode}>{t('pre-set')}</a> | <b>{t('custom')}</b></>
: <><b>{t('pre-set')}</b> | <a onClick={this.toggleCustomNode}>{t('custom')}</a></>
}
</div>
{
isCustomNode
? <Input
defaultValue={apiUrl}
isError={!isUrlValid}
label={t('remote node/endpoint to connect to')}
onChange={this.onChangeApiUrl}
/>
: <Dropdown
defaultValue={apiUrl}
label={t('remote node/endpoint to connect to')}
onChange={this.onChangeApiUrl}
options={uiSettings.availableNodes}
/>
}
</div>
</div>
<div className='ui--row'>
<div className='medium'>
<Dropdown
defaultValue={uiTheme}
label={t('default interface theme')}
onChange={this.onChangeUiTheme}
options={uiSettings.availableUIThemes}
/>
</div>
<div className='medium'>
<Dropdown
defaultValue={uiMode}
label={t('interface operation mode')}
onChange={this.onChangeUiMode}
options={uiSettings.availableUIModes}
/>
</div>
</div>
<div className='ui--row'>
<div className='full'>
<Dropdown
defaultValue={i18nLang}
isDisabled
label={t('default interface language')}
onChange={this.onChangeLang}
options={uiSettings.availableLanguages}
/>
</div>
</div>
<Button.Group>
<Button
isDisabled={!isUrlValid}
isPrimary
onClick={this.saveAndReload}
label={t('Save & Reload')}
/>
</Button.Group>
</>
);
}
private clearTypes = (): void => {
this.setState({
isTypesValid: true,
types: null,
typesPlaceholder: ''
});
}
private onChangeApiUrl = (apiUrl: string): void => {
this.setState(({ settings }: State) => ({
isUrlValid: this.isValidUrl(apiUrl),
settings: {
...settings,
apiUrl
@ -163,25 +228,19 @@ class App extends React.PureComponent<Props, State> {
typeRegistry.register(types);
this.setState(({ settings }: State) => ({
settings: {
...settings,
types,
typesError: false,
typesPlaceholder
}
}));
this.setState({
isTypesValid: true,
types,
typesPlaceholder
});
} catch (error) {
console.error('Registering types:', error);
this.setState(({ settings }: State) => ({
settings: {
...settings,
types: null,
typesError: true,
typesPlaceholder: error.message
}
}));
this.setState({
isTypesValid: false,
types: null,
typesPlaceholder: error.message
});
}
}
@ -204,49 +263,44 @@ class App extends React.PureComponent<Props, State> {
}
private toggleCustomNode = (): void => {
this.setState(({ settings }: State) => {
const customNode = !settings.customNode;
this.setState(({ isCustomNode, settings }: State) => {
// reset URL to a preset when toggled to preset
const apiUrl = customNode
? settings.apiUrl
: uiSettings.availableNodes[0].value;
const apiUrl = isCustomNode
? uiSettings.availableNodes[0].value
: settings.apiUrl;
return {
isCustomNode: !isCustomNode,
isUrlValid: true,
settings: {
...settings,
apiUrl,
customNode
apiUrl
}
};
});
}
private save = (): void => {
const { onStatusChange, t } = this.props;
const { settings: { types, typesError } } = this.state;
private isValidUrl (apiUrl: string): boolean {
return (
// some random length... we probably want to parse via some lib
(apiUrl.length >= 7) &&
// check that it starts with a valid ws identifier
(apiUrl.startsWith('ws://') || apiUrl.startsWith('wss://'))
);
}
// validate custom node url
const apiUrl = this.state.settings.apiUrl;
private saveDeveloper = (): void => {
const { isTypesValid, types } = this.state;
if (this.state.settings.customNode) {
if (!(apiUrl.startsWith('ws://localhost') ||
apiUrl.startsWith('ws://127.0.0.1') ||
apiUrl.startsWith('wss://'))) {
onStatusChange({
action: '',
status: 'error',
message: t('Custom node URL is not valid')
});
return;
}
}
uiSettings.set(this.state.settings);
if (types && !typesError) {
if (isTypesValid) {
store.set('types', types);
}
}
private saveAndReload = (): void => {
const { settings } = this.state;
uiSettings.set(settings);
// HACK This is terribe, but since the API needs to re-connect, but since
// the API does not yet handle re-connections properly, it is what it is

View File

@ -37,15 +37,11 @@ type State = {
};
class Account extends React.PureComponent<Props, State> {
constructor (props: Props) {
super(props);
this.state = {
isNominateOpen: false,
isNominating: false,
isPrefsOpen: false
};
}
state: State = {
isNominateOpen: false,
isNominating: false,
isPrefsOpen: false
};
static getDerivedStateFromProps ({ staking_nominating }: Props) {
const isNominating = !!staking_nominating && !staking_nominating.isEmpty;

View File

@ -24,17 +24,11 @@ type State = {
};
class Nominating extends React.PureComponent<Props, State> {
state: State;
constructor (props: Props) {
super(props);
this.state = {
isNomineeValid: false,
isAddressFormatValid: false,
nominee: ''
};
}
state: State = {
isNomineeValid: false,
isAddressFormatValid: false,
nominee: ''
};
render () {
const { isOpen, style } = this.props;

View File

@ -86,7 +86,7 @@ class App extends React.PureComponent<Props, State> {
);
}
private renderComponent = (Component: React.ComponentType<ComponentProps>) => {
private renderComponent (Component: React.ComponentType<ComponentProps>) {
return (): React.ReactNode => {
const { intentions, validators } = this.state;
const { balances = {} } = this.props;

View File

@ -20,16 +20,10 @@ type State = {
};
class Raw extends React.PureComponent<Props, State> {
state: State;
constructor (props: Props) {
super(props);
this.state = {
isValid: false,
key: new Uint8Array([])
};
}
state: State = {
isValid: false,
key: new Uint8Array([])
};
render () {
const { t } = this.props;

View File

@ -65,7 +65,7 @@ class Selection extends React.PureComponent<Props, State> {
);
}
private renderComponent = (Component: React.ComponentType<ComponentProps>) => {
private renderComponent (Component: React.ComponentType<ComponentProps>) {
return (): React.ReactNode => {
return (
<Component onAdd={this.onAdd} />

View File

@ -3,12 +3,15 @@
// of the Apache-2.0 license. See the LICENSE file for details.
import { AppProps, I18nProps } from '@polkadot/ui-app/types';
import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
import './index.css';
import React from 'react';
import { Route, Switch } from 'react-router';
import Tabs, { TabItem } from '@polkadot/ui-app/Tabs';
import accountObservable from '@polkadot/ui-keyring/observable/accounts';
import { withMulti, withObservable } from '@polkadot/ui-api/index';
import Hash from './Hash';
import Rpc from './Rpc';
@ -16,7 +19,9 @@ import Sign from './Sign';
import Verify from './Verify';
import translate from './translate';
type Props = AppProps & I18nProps;
type Props = AppProps & I18nProps & {
allAccounts?: SubjectInfo
};
type State = {
tabs: Array<TabItem>
@ -32,7 +37,7 @@ class ToolboxApp extends React.PureComponent<Props, State> {
tabs: [
{
name: 'rpc',
text: 'RPC calls'
text: t('RPC calls')
},
{
name: 'hash',
@ -50,15 +55,21 @@ class ToolboxApp extends React.PureComponent<Props, State> {
};
}
render () {
const { basePath } = this.props;
const { allAccounts = {}, basePath } = this.props;
const { tabs } = this.state;
const hasAccounts = Object.keys(allAccounts).length !== 0;
const filteredTabs = hasAccounts
? tabs
: tabs.filter(({ name }) =>
!['sign', 'verify'].includes(name)
);
return (
<main className='toolbox--App'>
<header>
<Tabs
basePath={basePath}
items={tabs}
items={filteredTabs}
/>
</header>
<Switch>
@ -72,4 +83,8 @@ class ToolboxApp extends React.PureComponent<Props, State> {
}
}
export default translate(ToolboxApp);
export default withMulti(
ToolboxApp,
translate,
withObservable(accountObservable.subject, { propName: 'allAccounts' })
);

View File

@ -31,19 +31,13 @@ type State = {
const ZERO = new BN(0);
class Transfer extends React.PureComponent<Props, State> {
state: State;
constructor (props: Props) {
super(props);
this.state = {
accountId: null,
amount: ZERO,
extrinsic: null,
hasAvailable: true,
recipientId: null
};
}
state: State = {
accountId: null,
amount: ZERO,
extrinsic: null,
hasAvailable: true,
recipientId: null
};
render () {
const { t } = this.props;

View File

@ -3,12 +3,8 @@
// of the Apache-2.0 license. See the LICENSE file for details.
import { AppProps, I18nProps } from '@polkadot/ui-app/types';
import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
import React from 'react';
import { Trans } from 'react-i18next';
import accountObservable from '@polkadot/ui-keyring/observable/accounts';
import { withMulti, withObservable } from '@polkadot/ui-api/index';
import { Tabs } from '@polkadot/ui-app/index';
import './index.css';
@ -16,34 +12,14 @@ import './index.css';
import Transfer from './Transfer';
import translate from './translate';
type Props = AppProps & I18nProps & {
allAccounts?: SubjectInfo
};
type Props = AppProps & I18nProps;
class App extends React.PureComponent<Props> {
render () {
const { basePath, t } = this.props;
return (
<main className='transfer--App'>
{this.renderBody()}
</main>
);
}
private renderBody () {
const { allAccounts, basePath, t } = this.props;
if (!allAccounts || !Object.keys(allAccounts).length) {
return (
<div className='transfer--App-no-accounts'>
<Trans i18nKey='no-accounts'>
You currently have no active accounts. Before you are able to transfer, add <a href='#/accounts'>an account</a>.
</Trans>
</div>
);
}
return (
<>
<header>
<Tabs
basePath={basePath}
@ -54,13 +30,9 @@ class App extends React.PureComponent<Props> {
/>
</header>
<Transfer />
</>
</main>
);
}
}
export default withMulti(
App,
translate,
withObservable(accountObservable.subject, { propName: 'allAccounts' })
);
export default translate(App);

View File

@ -23,7 +23,9 @@ type Props = I18nProps & ApiProps & {
};
const unknown = {
isApiGated: false,
display: {
needsApi: undefined
},
Component: NotFound,
name: ''
};
@ -32,11 +34,11 @@ class Content extends React.Component<Props> {
render () {
const { isApiConnected, isApiReady, location, t } = this.props;
const app = location.pathname.slice(1) || '';
const { Component, isApiGated, name } = routing.routes.find((route) =>
const { Component, display: { needsApi }, name } = routing.routes.find((route) =>
!!(route && app.indexOf(route.name) === 0)
) || unknown;
if (isApiGated && (!isApiReady || !isApiConnected)) {
if (needsApi && (!isApiReady || !isApiConnected)) {
return (
<div className='apps--Content-body'>
<main>{t('Waiting for API to be connected and ready.')}</main>
@ -51,6 +53,7 @@ class Content extends React.Component<Props> {
<Component
key='content-content'
basePath={`/${name}`}
location={location}
onStatusChange={queueAction}
/>,
<Status
@ -67,12 +70,12 @@ class Content extends React.Component<Props> {
}
// React-router needs to be first, otherwise we have blocked updates
// These API queries are used in a number of places, warm them up as
// to avoid constant un/resubscriptions on these
export default withMulti(
Content,
withRouter,
translate,
// These API queries are used in a number of places, warm them up
// to avoid constant un-/re-subscribe on these
withCalls<Props>(
'query.session.validators',
'derive.accounts.indexes',

View File

@ -4,16 +4,19 @@
import { I18nProps } from '@polkadot/ui-app/types';
import { ApiProps } from '@polkadot/ui-api/types';
import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
import { Route } from '../types';
import React from 'react';
import { withRouter } from 'react-router';
import { NavLink } from 'react-router-dom';
import { Icon, Menu } from '@polkadot/ui-app/index';
import { withApi, withMulti } from '@polkadot/ui-api/index';
import accountObservable from '@polkadot/ui-keyring/observable/accounts';
import { withApi, withMulti, withObservable } from '@polkadot/ui-api/index';
import { isFunction } from '@polkadot/util';
type Props = I18nProps & ApiProps & {
allAccounts?: SubjectInfo,
route: Route
};
@ -21,7 +24,7 @@ class Item extends React.PureComponent<Props> {
render () {
const { route: { i18n, icon, name }, t } = this.props;
if (!this.isApiAvailable()) {
if (!this.isVisible()) {
return null;
}
@ -38,13 +41,18 @@ class Item extends React.PureComponent<Props> {
);
}
private isApiAvailable () {
const { api, isApiConnected, isApiReady, route: { isApiGated, name, needsApi } } = this.props;
private isVisible () {
const { allAccounts = {}, api, isApiConnected, isApiReady, route: { display: { isHidden, needsAccounts, needsApi }, name } } = this.props;
const hasAccounts = Object.keys(allAccounts).length !== 0;
if (isApiGated && (!isApiReady || !isApiConnected)) {
if (isHidden) {
return false;
} else if (!needsApi || !needsApi.length) {
} else if (needsAccounts && !hasAccounts) {
return false;
} else if (!needsApi) {
return true;
} else if (!isApiReady || !isApiConnected) {
return false;
}
const notFound = needsApi.filter((endpoint) => {
@ -68,5 +76,6 @@ class Item extends React.PureComponent<Props> {
export default withMulti(
Item,
withRouter,
withApi
withApi,
withObservable(accountObservable.subject, { propName: 'allAccounts' })
);

View File

@ -62,26 +62,22 @@ class SideBar extends React.PureComponent<Props> {
private renderRoutes () {
const { t } = this.props;
return routing.routes
.filter((route) =>
!route || !route.isHidden
)
.map((route, index) => (
route
? (
<Item
key={route.name}
t={t}
route={route}
/>
)
: (
<Menu.Divider
hidden
key={index}
/>
)
));
return routing.routes.map((route, index) => (
route
? (
<Item
key={route.name}
t={t}
route={route}
/>
)
: (
<Menu.Divider
hidden
key={index}
/>
)
));
}
private renderGithub () {

View File

@ -9,12 +9,17 @@ import Template from '@polkadot/app-123code/index';
export default ([
{
Component: Template,
display: {
isHidden: true,
needsAccounts: true,
needsApi: [
'tx.balances.transfer'
]
},
i18n: {
defaultValue: 'Template'
},
icon: 'th',
isApiGated: true,
isHidden: true,
name: '123code'
}
] as Routes);

View File

@ -9,12 +9,13 @@ import Accounts from '@polkadot/app-accounts/index';
export default ([
{
Component: Accounts,
display: {
needsApi: []
},
i18n: {
defaultValue: 'Accounts'
},
icon: 'users',
isApiGated: true,
isHidden: false,
name: 'accounts'
}
] as Routes);

View File

@ -9,12 +9,13 @@ import Addresses from '@polkadot/app-addresses/index';
export default ([
{
Component: Addresses,
display: {
needsApi: []
},
i18n: {
defaultValue: 'Addresses'
},
icon: 'address book',
isApiGated: true,
isHidden: false,
name: 'addresses'
}
] as Routes);

View File

@ -9,15 +9,16 @@ import Democracy from '@polkadot/app-democracy/index';
export default ([
{
Component: Democracy,
display: {
needsAccounts: true,
needsApi: [
'query.democracy.nextTally'
]
},
i18n: {
defaultValue: 'Democracy'
},
icon: 'calendar check',
isApiGated: true,
isHidden: false,
name: 'democracy',
needsApi: [
'query.democracy.nextTally'
]
name: 'democracy'
}
] as Routes);

View File

@ -9,12 +9,13 @@ import Explorer from '@polkadot/app-explorer/index';
export default ([
{
Component: Explorer,
display: {
needsApi: []
},
i18n: {
defaultValue: 'Explorer'
},
icon: 'braille',
isApiGated: true,
isHidden: false,
name: 'explorer'
}
] as Routes);

View File

@ -9,12 +9,14 @@ import Extrinsics from '@polkadot/app-extrinsics/index';
export default ([
{
Component: Extrinsics,
display: {
needsAccounts: true,
needsApi: []
},
i18n: {
defaultValue: 'Extrinsics'
},
icon: 'send',
isApiGated: true,
isHidden: false,
name: 'extrinsics'
}
] as Routes);

View File

@ -9,12 +9,13 @@ import Nodeinfo from '@polkadot/app-nodeinfo/index';
export default ([
{
Component: Nodeinfo,
display: {
needsApi: []
},
i18n: {
defaultValue: 'Node info'
},
icon: 'tty',
isApiGated: true,
isHidden: false,
name: 'nodeinfo'
}
] as Routes);

View File

@ -9,12 +9,13 @@ import Settings from '@polkadot/app-settings/index';
export default ([
{
Component: Settings,
display: {
needsApi: []
},
i18n: {
defaultValue: 'Settings'
},
icon: 'settings',
isApiGated: false,
isHidden: false,
name: 'settings'
}
] as Routes);

View File

@ -9,15 +9,16 @@ import Staking from '@polkadot/app-staking/index';
export default ([
{
Component: Staking,
display: {
needsAccounts: true,
needsApi: [
'tx.staking.stake'
]
},
i18n: {
defaultValue: 'Staking'
},
icon: 'certificate',
isApiGated: true,
isHidden: false,
name: 'staking',
needsApi: [
'tx.staking.stake'
]
name: 'staking'
}
] as Routes);

View File

@ -9,12 +9,13 @@ import Storage from '@polkadot/app-storage/index';
export default ([
{
Component: Storage,
display: {
needsApi: []
},
i18n: {
defaultValue: 'Chain state'
},
icon: 'database',
isApiGated: true,
isHidden: false,
name: 'chainstate'
}
] as Routes);

View File

@ -9,12 +9,13 @@ import Toolbox from '@polkadot/app-toolbox/index';
export default ([
{
Component: Toolbox,
display: {
needsApi: []
},
i18n: {
defaultValue: 'Toolbox'
},
icon: 'configure',
isApiGated: true,
isHidden: false,
name: 'toolbox'
}
] as Routes);

View File

@ -9,15 +9,16 @@ import Transfer from '@polkadot/app-transfer/index';
export default ([
{
Component: Transfer,
display: {
needsAccounts: true,
needsApi: [
'tx.balances.transfer'
]
},
i18n: {
defaultValue: 'Transfer'
},
icon: 'angle double right',
isApiGated: true,
isHidden: false,
name: 'transfer',
needsApi: [
'tx.balances.transfer'
]
name: 'transfer'
}
] as Routes);

View File

@ -9,12 +9,14 @@ export type RouteProps = AppProps & BareProps;
export type Route = {
Component: React.ComponentType<RouteProps>,
i18n: any, // I18Next$Translate$Config,
display: {
isHidden?: boolean,
needsAccounts?: boolean,
needsApi?: Array<string>
},
i18n: { defaultValue: string },
icon: SemanticICONS,
isApiGated: boolean,
isHidden: boolean,
name: string,
needsApi?: Array<string>
name: string
};
export type Routes = Array<Route | null>;

View File

@ -26,7 +26,11 @@ const NOOP = () => {
export default function withCall<P extends ApiProps> (endpoint: string, { at, atProp, callOnResult, params = [], paramName = 'params', propName, transform = echoTransform }: Options = {}): (Inner: React.ComponentType<ApiProps>) => React.ComponentType<any> {
return (Inner: React.ComponentType<ApiProps>): React.ComponentType<Subtract<P, ApiProps>> => {
class WithPromise extends React.Component<P, State> {
state: State;
state: State = {
callResult: void 0,
callUpdated: false,
callUpdatedAt: 0
};
private destroy?: () => void;
private isActive: boolean = false;
private propName: string;
@ -38,11 +42,6 @@ export default function withCall<P extends ApiProps> (endpoint: string, { at, at
const [, section, method] = endpoint.split('.');
this.propName = `${section}_${method}`;
this.state = {
callResult: void 0,
callUpdated: false,
callUpdatedAt: 0
};
}
componentDidUpdate (prevProps: any) {

View File

@ -23,18 +23,12 @@ type State = CallState & {
export default function withObservable<T, P> (observable: Observable<P>, { callOnResult, propName = 'value', transform = echoTransform }: Options = {}): HOC {
return (Inner: React.ComponentType<any>, defaultProps: DefaultProps = {}, render?: RenderFn): React.ComponentType<any> => {
return class WithObservable extends React.Component<any, State> {
state: State;
constructor (props: any) {
super(props);
this.state = {
callResult: void 0,
callUpdated: false,
callUpdatedAt: 0,
subscriptions: []
};
}
state: State = {
callResult: void 0,
callUpdated: false,
callUpdatedAt: 0,
subscriptions: []
};
componentDidMount () {
this.setState({

View File

@ -76,11 +76,7 @@ const createOption = (address: string) => {
};
class InputAddress extends React.PureComponent<Props, State> {
constructor (props: Props) {
super(props);
this.state = {};
}
state: State = {};
static getDerivedStateFromProps ({ value }: Props): State | null {
try {
@ -112,8 +108,9 @@ class InputAddress extends React.PureComponent<Props, State> {
render () {
const { className, defaultValue, hideAddress = false, isDisabled = false, isError, label, optionsAll, type = DEFAULT_TYPE, style, withLabel } = this.props;
const { value } = this.state;
const hasOptions = optionsAll && Object.keys(optionsAll[type]).length !== 0;
if (!optionsAll || !Object.keys(optionsAll[type]).length) {
if (!hasOptions && !isDisabled) {
return null;
}
@ -143,7 +140,7 @@ class InputAddress extends React.PureComponent<Props, State> {
options={
isDisabled && actualValue
? [createOption(actualValue)]
: optionsAll[type]
: (optionsAll ? optionsAll[type] : [])
}
style={style}
value={value}

View File

@ -16,6 +16,7 @@ type Props = BareProps & WithNamespaces & {
// Reference Example Usage: https://github.com/react-dropzone/react-dropzone/tree/master/examples/Accept
// i.e. MIME types: 'application/json, text/plain', or '.json, .txt'
accept?: string,
clearContent?: boolean,
isDisabled?: boolean,
isError?: boolean,
label: string,
@ -41,7 +42,7 @@ class InputFile extends React.PureComponent<Props, State> {
state: State = {};
render () {
const { accept, className, isDisabled, isError = false, label, placeholder, t, withLabel } = this.props;
const { accept, className, clearContent, isDisabled, isError = false, label, placeholder, t, withLabel } = this.props;
const { file } = this.state;
return (
@ -58,7 +59,7 @@ class InputFile extends React.PureComponent<Props, State> {
>
<div className='label'>
{
!file
!file || clearContent
? placeholder || t('drag and drop the file here')
: placeholder || t('{{name}} ({{size}} bytes)', {
replace: file

View File

@ -45,14 +45,17 @@ class Tabs extends React.PureComponent<Props> {
const to = index === 0
? basePath
: `${basePath}/${name}`;
// only do exact matching when not the fallback (first position tab),
// params are problematic for dynamic hidden such as app-accounts
const isExact = !hasParams || index === 0;
return (
<NavLink
activeClassName='active'
className='item'
exact={!hasParams}
exact={isExact}
key={to}
strict={!hasParams}
strict={isExact}
to={to}
>
{text}

View File

@ -15,6 +15,7 @@ export type BareProps = {
export type AppProps = {
basePath: string,
location: Location,
onStatusChange: (status: ActionStatus) => void
};

View File

@ -16,13 +16,9 @@ type State = {
};
export default class Bytes extends React.PureComponent<Props, State> {
constructor (props: Props) {
super(props);
this.state = {
isFileDrop: false
};
}
state: State = {
isFileDrop: false
};
render () {
const { isDisabled } = this.props;

View File

@ -20,16 +20,12 @@ type State = {
};
export default class Tuple extends React.PureComponent<Props, State> {
constructor (props: Props) {
super(props);
this.state = {
Components: [],
sub: [],
subTypes: [],
values: []
};
}
state: State = {
Components: [],
sub: [],
subTypes: [],
values: []
};
static getDerivedStateFromProps ({ defaultValue: { value }, type: { sub, type } }: Props, prevState: State): Partial<State> | null {
if (type === prevState.type) {

View File

@ -24,14 +24,10 @@ type State = {
};
class Vector extends React.PureComponent<Props, State> {
constructor (props: Props) {
super(props);
this.state = {
Component: null,
values: []
};
}
state: State = {
Component: null,
values: []
};
static getDerivedStateFromProps ({ defaultValue: { value = [] }, isDisabled, type: { sub, type } }: Props, prevState: State): Partial<State> | null {
if (type === prevState.type) {

View File

@ -26,16 +26,12 @@ type State = ExtraFees & {
};
class Proposal extends React.PureComponent<Props, State> {
constructor (props: Props) {
super(props);
this.state = {
extraFees: new BN(0),
extraAmount: new BN(0),
extraWarn: false,
isBelowMinimum: false
};
}
state: State = {
extraFees: new BN(0),
extraAmount: new BN(0),
extraWarn: false,
isBelowMinimum: false
};
static getDerivedStateFromProps ({ deposit, democracy_minimumDeposit = new BN(0), onChange }: Props): State {
const extraAmount = deposit instanceof Compact

View File

@ -30,17 +30,13 @@ type State = ExtraFees & {
};
class Transfer extends React.PureComponent<Props, State> {
constructor (props: Props) {
super(props);
this.state = {
extraFees: new BN(0),
extraAmount: new BN(0),
extraWarn: false,
isCreation: false,
isNoEffect: false
};
}
state: State = {
extraFees: new BN(0),
extraAmount: new BN(0),
extraWarn: false,
isCreation: false,
isNoEffect: false
};
static getDerivedStateFromProps ({ amount, balances_votingBalance = ZERO_BALANCE, fees, onChange }: Props): State {
let extraFees = fees.transferFee;

View File

@ -39,16 +39,10 @@ type State = {
};
class Signer extends React.PureComponent<Props, State> {
state: State;
constructor (props: Props) {
super(props);
this.state = {
password: '',
unlockError: null
};
}
state: State = {
password: '',
unlockError: null
};
static getDerivedStateFromProps ({ queue }: Props, { currentItem, password, unlockError }: State): State {
const nextItem = queue.find(({ status }) =>

View File

@ -11,5 +11,6 @@
"Transaction": {
"inputs": "Vec<TransactionInput>",
"outputs": "Vec<TransactionOutput>"
}
},
"blah": "u32"
}