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> <section>
<h1>transfer</h1> <h1>transfer</h1>
<div className='ui--row'> <div className='ui--row'>
<article className='medium'> <div className='large'>
<InputAddress <InputAddress
label='recipient address for this transfer' label='recipient address for this transfer'
onChange={this.onChangeRecipient} onChange={this.onChangeRecipient}
@ -45,7 +45,7 @@ export default class Transfer extends React.PureComponent<Props> {
tx='balances.transfer' tx='balances.transfer'
/> />
</Button.Group> </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 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> </div>
</section> </section>

View File

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

View File

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

View File

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

View File

@ -27,7 +27,7 @@ type Props = AppProps & I18nProps & {
type State = { type State = {
hidden: Array<string>, hidden: Array<string>,
items: Array<TabItem> tabs: Array<TabItem>
}; };
class AccountsApp extends React.PureComponent<Props, State> { class AccountsApp extends React.PureComponent<Props, State> {
@ -43,7 +43,7 @@ class AccountsApp extends React.PureComponent<Props, State> {
this.state = { this.state = {
...baseState, ...baseState,
items: [ tabs: [
{ {
name: 'edit', name: 'edit',
text: t('Edit account') text: t('Edit account')
@ -72,8 +72,10 @@ class AccountsApp extends React.PureComponent<Props, State> {
} }
static hideEditState () { 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 { return {
hidden: ['edit'] hidden: ['edit', 'vanity']
}; };
} }
@ -93,7 +95,7 @@ class AccountsApp extends React.PureComponent<Props, State> {
render () { render () {
const { basePath } = this.props; const { basePath } = this.props;
const { hidden, items } = this.state; const { hidden, tabs } = this.state;
const renderCreator = this.renderComponent(Creator); const renderCreator = this.renderComponent(Creator);
return ( return (
@ -102,7 +104,7 @@ class AccountsApp extends React.PureComponent<Props, State> {
<Tabs <Tabs
basePath={basePath} basePath={basePath}
hidden={hidden} hidden={hidden}
items={items} items={tabs}
/> />
</header> </header>
<Switch> <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) => { return ({ match }: LocationProps) => {
const { basePath, onStatusChange } = this.props; const { basePath, location, onStatusChange } = this.props;
return ( return (
<Component <Component
basePath={basePath} basePath={basePath}
location={location}
match={match} match={match}
onStatusChange={onStatusChange} 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 () => { return () => {
const { basePath, onStatusChange } = this.props; const { basePath, location, onStatusChange } = this.props;
return ( return (
<Component <Component
basePath={basePath} basePath={basePath}
location={location}
onStatusChange={onStatusChange} onStatusChange={onStatusChange}
/> />
); );

View File

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

View File

@ -24,14 +24,10 @@ type State = {
}; };
class EventsRecent extends React.PureComponent<Props, State> { class EventsRecent extends React.PureComponent<Props, State> {
constructor (props: Props) { state: State = {
super(props); prevEventHash: '',
recentEvents: []
this.state = { };
prevEventHash: '',
recentEvents: []
};
}
static getDerivedStateFromProps ({ system_events = [] }: Props, prevState: State): State | null { static getDerivedStateFromProps ({ system_events = [] }: Props, prevState: State): State | null {
const prevEventHash = xxhashAsHex(stringToU8a(JSON.stringify(system_events))); 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 './index.css';
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom';
import { QueueConsumer } from '@polkadot/ui-app/Status/Context'; import { QueueConsumer } from '@polkadot/ui-app/Status/Context';
import { Tabs } from '@polkadot/ui-app/index'; 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 Selection from './Selection';
import translate from './translate'; import translate from './translate';
type Props = AppProps & I18nProps & { type Props = AppProps & I18nProps;
accountAll?: Array<any>
};
class ExtrinsicsApp extends React.PureComponent<Props> { class ExtrinsicsApp extends React.PureComponent<Props> {
render () { render () {
const { accountAll, basePath, t } = this.props; const { 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>
);
}
return ( return (
<main className='extrinsics--App'> <main className='extrinsics--App'>
@ -59,8 +43,4 @@ class ExtrinsicsApp extends React.PureComponent<Props> {
export { ExtrinsicsApp }; export { ExtrinsicsApp };
export default withMulti( export default translate(ExtrinsicsApp);
ExtrinsicsApp,
translate,
withObservable(accountsObservable.subject, { propName: 'accountAll' })
);

View File

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

View File

@ -3,12 +3,14 @@
// of the Apache-2.0 license. See the LICENSE file for details. // of the Apache-2.0 license. See the LICENSE file for details.
import { AppProps, I18nProps } from '@polkadot/ui-app/types'; import { AppProps, I18nProps } from '@polkadot/ui-app/types';
import { TabItem } from '@polkadot/ui-app/Tabs';
import { SettingsStruct } from '@polkadot/ui-settings/types'; import { SettingsStruct } from '@polkadot/ui-settings/types';
import React from 'react'; import React from 'react';
import { Route, Switch } from 'react-router';
import store from 'store'; import store from 'store';
import typeRegistry from '@polkadot/types/codec/typeRegistry'; 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 uiSettings from '@polkadot/ui-settings';
import { u8aToString } from '@polkadot/util'; import { u8aToString } from '@polkadot/util';
@ -19,130 +21,193 @@ import translate from './translate';
type Props = AppProps & I18nProps; type Props = AppProps & I18nProps;
type State = { type State = {
settings: SettingsStruct & { isCustomNode: boolean,
types?: { [index: string]: any } | null, isTypesValid: boolean,
typesError?: boolean, isUrlValid: boolean,
typesPlaceholder?: string, settings: SettingsStruct,
customNode: boolean tabs: Array<TabItem>,
} types?: { [index: string]: any } | null,
typesPlaceholder?: string
}; };
class App extends React.PureComponent<Props, State> { class App extends React.PureComponent<Props, State> {
constructor (props: Props) { constructor (props: Props) {
super(props); super(props);
const { t } = props;
const types = store.get('types') || {}; const types = store.get('types') || {};
const names = Object.keys(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 // 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++) { for (let i = 0; i < uiSettings.availableNodes.length; i++) {
if (uiSettings.availableNodes[i].value === presets.apiUrl) { if (uiSettings.availableNodes[i].value === settings.apiUrl) {
customNode = false; isCustomNode = false;
} }
} }
this.state = { this.state = {
settings: { isCustomNode,
...presets, isTypesValid: true,
typesPlaceholder: names.length isUrlValid: this.isValidUrl(settings.apiUrl),
? names.join(', ') tabs: [
: undefined, {
customNode: customNode name: 'general',
} text: t('General')
},
{
name: 'developer',
text: t('Developer')
}
],
typesPlaceholder: names.length
? names.join(', ')
: undefined,
settings
}; };
} }
render () { render () {
const { t } = this.props; const { basePath } = this.props;
const { settings: { apiUrl, i18nLang, typesPlaceholder, typesError, uiMode, uiTheme, customNode } } = this.state; const { tabs } = this.state;
return ( return (
<main className='settings--App'> <main className='settings--App'>
<section> <header>
<h1>{t('general')}</h1> <Tabs
<div className='ui--row'> basePath={basePath}
<div className='full'> items={tabs}
<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')}
/> />
</Button.Group> </header>
<Switch>
<Route path={`${basePath}/developer`} render={this.renderDeveloper} />
<Route render={this.renderGeneral} />
</Switch>
</main> </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 => { private onChangeApiUrl = (apiUrl: string): void => {
this.setState(({ settings }: State) => ({ this.setState(({ settings }: State) => ({
isUrlValid: this.isValidUrl(apiUrl),
settings: { settings: {
...settings, ...settings,
apiUrl apiUrl
@ -163,25 +228,19 @@ class App extends React.PureComponent<Props, State> {
typeRegistry.register(types); typeRegistry.register(types);
this.setState(({ settings }: State) => ({ this.setState({
settings: { isTypesValid: true,
...settings, types,
types, typesPlaceholder
typesError: false, });
typesPlaceholder
}
}));
} catch (error) { } catch (error) {
console.error('Registering types:', error); console.error('Registering types:', error);
this.setState(({ settings }: State) => ({ this.setState({
settings: { isTypesValid: false,
...settings, types: null,
types: null, typesPlaceholder: error.message
typesError: true, });
typesPlaceholder: error.message
}
}));
} }
} }
@ -204,49 +263,44 @@ class App extends React.PureComponent<Props, State> {
} }
private toggleCustomNode = (): void => { private toggleCustomNode = (): void => {
this.setState(({ settings }: State) => { this.setState(({ isCustomNode, settings }: State) => {
const customNode = !settings.customNode;
// reset URL to a preset when toggled to preset // reset URL to a preset when toggled to preset
const apiUrl = customNode const apiUrl = isCustomNode
? settings.apiUrl ? uiSettings.availableNodes[0].value
: uiSettings.availableNodes[0].value; : settings.apiUrl;
return { return {
isCustomNode: !isCustomNode,
isUrlValid: true,
settings: { settings: {
...settings, ...settings,
apiUrl, apiUrl
customNode
} }
}; };
}); });
} }
private save = (): void => { private isValidUrl (apiUrl: string): boolean {
const { onStatusChange, t } = this.props; return (
const { settings: { types, typesError } } = this.state; // 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 private saveDeveloper = (): void => {
const apiUrl = this.state.settings.apiUrl; const { isTypesValid, types } = this.state;
if (this.state.settings.customNode) { if (isTypesValid) {
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) {
store.set('types', types); 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 // 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 // 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> { class Account extends React.PureComponent<Props, State> {
constructor (props: Props) { state: State = {
super(props); isNominateOpen: false,
isNominating: false,
this.state = { isPrefsOpen: false
isNominateOpen: false, };
isNominating: false,
isPrefsOpen: false
};
}
static getDerivedStateFromProps ({ staking_nominating }: Props) { static getDerivedStateFromProps ({ staking_nominating }: Props) {
const isNominating = !!staking_nominating && !staking_nominating.isEmpty; const isNominating = !!staking_nominating && !staking_nominating.isEmpty;

View File

@ -24,17 +24,11 @@ type State = {
}; };
class Nominating extends React.PureComponent<Props, State> { class Nominating extends React.PureComponent<Props, State> {
state: State; state: State = {
isNomineeValid: false,
constructor (props: Props) { isAddressFormatValid: false,
super(props); nominee: ''
};
this.state = {
isNomineeValid: false,
isAddressFormatValid: false,
nominee: ''
};
}
render () { render () {
const { isOpen, style } = this.props; 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 => { return (): React.ReactNode => {
const { intentions, validators } = this.state; const { intentions, validators } = this.state;
const { balances = {} } = this.props; const { balances = {} } = this.props;

View File

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

View File

@ -3,12 +3,15 @@
// of the Apache-2.0 license. See the LICENSE file for details. // of the Apache-2.0 license. See the LICENSE file for details.
import { AppProps, I18nProps } from '@polkadot/ui-app/types'; import { AppProps, I18nProps } from '@polkadot/ui-app/types';
import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
import './index.css'; import './index.css';
import React from 'react'; import React from 'react';
import { Route, Switch } from 'react-router'; import { Route, Switch } from 'react-router';
import Tabs, { TabItem } from '@polkadot/ui-app/Tabs'; 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 Hash from './Hash';
import Rpc from './Rpc'; import Rpc from './Rpc';
@ -16,7 +19,9 @@ import Sign from './Sign';
import Verify from './Verify'; import Verify from './Verify';
import translate from './translate'; import translate from './translate';
type Props = AppProps & I18nProps; type Props = AppProps & I18nProps & {
allAccounts?: SubjectInfo
};
type State = { type State = {
tabs: Array<TabItem> tabs: Array<TabItem>
@ -32,7 +37,7 @@ class ToolboxApp extends React.PureComponent<Props, State> {
tabs: [ tabs: [
{ {
name: 'rpc', name: 'rpc',
text: 'RPC calls' text: t('RPC calls')
}, },
{ {
name: 'hash', name: 'hash',
@ -50,15 +55,21 @@ class ToolboxApp extends React.PureComponent<Props, State> {
}; };
} }
render () { render () {
const { basePath } = this.props; const { allAccounts = {}, basePath } = this.props;
const { tabs } = this.state; const { tabs } = this.state;
const hasAccounts = Object.keys(allAccounts).length !== 0;
const filteredTabs = hasAccounts
? tabs
: tabs.filter(({ name }) =>
!['sign', 'verify'].includes(name)
);
return ( return (
<main className='toolbox--App'> <main className='toolbox--App'>
<header> <header>
<Tabs <Tabs
basePath={basePath} basePath={basePath}
items={tabs} items={filteredTabs}
/> />
</header> </header>
<Switch> <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); const ZERO = new BN(0);
class Transfer extends React.PureComponent<Props, State> { class Transfer extends React.PureComponent<Props, State> {
state: State; state: State = {
accountId: null,
constructor (props: Props) { amount: ZERO,
super(props); extrinsic: null,
hasAvailable: true,
this.state = { recipientId: null
accountId: null, };
amount: ZERO,
extrinsic: null,
hasAvailable: true,
recipientId: null
};
}
render () { render () {
const { t } = this.props; const { t } = this.props;

View File

@ -3,12 +3,8 @@
// of the Apache-2.0 license. See the LICENSE file for details. // of the Apache-2.0 license. See the LICENSE file for details.
import { AppProps, I18nProps } from '@polkadot/ui-app/types'; import { AppProps, I18nProps } from '@polkadot/ui-app/types';
import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
import React from 'react'; 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 { Tabs } from '@polkadot/ui-app/index';
import './index.css'; import './index.css';
@ -16,34 +12,14 @@ import './index.css';
import Transfer from './Transfer'; import Transfer from './Transfer';
import translate from './translate'; import translate from './translate';
type Props = AppProps & I18nProps & { type Props = AppProps & I18nProps;
allAccounts?: SubjectInfo
};
class App extends React.PureComponent<Props> { class App extends React.PureComponent<Props> {
render () { render () {
const { basePath, t } = this.props;
return ( return (
<main className='transfer--App'> <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> <header>
<Tabs <Tabs
basePath={basePath} basePath={basePath}
@ -54,13 +30,9 @@ class App extends React.PureComponent<Props> {
/> />
</header> </header>
<Transfer /> <Transfer />
</> </main>
); );
} }
} }
export default withMulti( export default translate(App);
App,
translate,
withObservable(accountObservable.subject, { propName: 'allAccounts' })
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,12 +9,14 @@ export type RouteProps = AppProps & BareProps;
export type Route = { export type Route = {
Component: React.ComponentType<RouteProps>, Component: React.ComponentType<RouteProps>,
i18n: any, // I18Next$Translate$Config, display: {
isHidden?: boolean,
needsAccounts?: boolean,
needsApi?: Array<string>
},
i18n: { defaultValue: string },
icon: SemanticICONS, icon: SemanticICONS,
isApiGated: boolean, name: string
isHidden: boolean,
name: string,
needsApi?: Array<string>
}; };
export type Routes = Array<Route | null>; 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> { 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>> => { return (Inner: React.ComponentType<ApiProps>): React.ComponentType<Subtract<P, ApiProps>> => {
class WithPromise extends React.Component<P, State> { class WithPromise extends React.Component<P, State> {
state: State; state: State = {
callResult: void 0,
callUpdated: false,
callUpdatedAt: 0
};
private destroy?: () => void; private destroy?: () => void;
private isActive: boolean = false; private isActive: boolean = false;
private propName: string; private propName: string;
@ -38,11 +42,6 @@ export default function withCall<P extends ApiProps> (endpoint: string, { at, at
const [, section, method] = endpoint.split('.'); const [, section, method] = endpoint.split('.');
this.propName = `${section}_${method}`; this.propName = `${section}_${method}`;
this.state = {
callResult: void 0,
callUpdated: false,
callUpdatedAt: 0
};
} }
componentDidUpdate (prevProps: any) { 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 { 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 (Inner: React.ComponentType<any>, defaultProps: DefaultProps = {}, render?: RenderFn): React.ComponentType<any> => {
return class WithObservable extends React.Component<any, State> { return class WithObservable extends React.Component<any, State> {
state: State; state: State = {
callResult: void 0,
constructor (props: any) { callUpdated: false,
super(props); callUpdatedAt: 0,
subscriptions: []
this.state = { };
callResult: void 0,
callUpdated: false,
callUpdatedAt: 0,
subscriptions: []
};
}
componentDidMount () { componentDidMount () {
this.setState({ this.setState({

View File

@ -76,11 +76,7 @@ const createOption = (address: string) => {
}; };
class InputAddress extends React.PureComponent<Props, State> { class InputAddress extends React.PureComponent<Props, State> {
constructor (props: Props) { state: State = {};
super(props);
this.state = {};
}
static getDerivedStateFromProps ({ value }: Props): State | null { static getDerivedStateFromProps ({ value }: Props): State | null {
try { try {
@ -112,8 +108,9 @@ class InputAddress extends React.PureComponent<Props, State> {
render () { render () {
const { className, defaultValue, hideAddress = false, isDisabled = false, isError, label, optionsAll, type = DEFAULT_TYPE, style, withLabel } = this.props; const { className, defaultValue, hideAddress = false, isDisabled = false, isError, label, optionsAll, type = DEFAULT_TYPE, style, withLabel } = this.props;
const { value } = this.state; 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; return null;
} }
@ -143,7 +140,7 @@ class InputAddress extends React.PureComponent<Props, State> {
options={ options={
isDisabled && actualValue isDisabled && actualValue
? [createOption(actualValue)] ? [createOption(actualValue)]
: optionsAll[type] : (optionsAll ? optionsAll[type] : [])
} }
style={style} style={style}
value={value} 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 // 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' // i.e. MIME types: 'application/json, text/plain', or '.json, .txt'
accept?: string, accept?: string,
clearContent?: boolean,
isDisabled?: boolean, isDisabled?: boolean,
isError?: boolean, isError?: boolean,
label: string, label: string,
@ -41,7 +42,7 @@ class InputFile extends React.PureComponent<Props, State> {
state: State = {}; state: State = {};
render () { 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; const { file } = this.state;
return ( return (
@ -58,7 +59,7 @@ class InputFile extends React.PureComponent<Props, State> {
> >
<div className='label'> <div className='label'>
{ {
!file !file || clearContent
? placeholder || t('drag and drop the file here') ? placeholder || t('drag and drop the file here')
: placeholder || t('{{name}} ({{size}} bytes)', { : placeholder || t('{{name}} ({{size}} bytes)', {
replace: file replace: file

View File

@ -45,14 +45,17 @@ class Tabs extends React.PureComponent<Props> {
const to = index === 0 const to = index === 0
? basePath ? basePath
: `${basePath}/${name}`; : `${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 ( return (
<NavLink <NavLink
activeClassName='active' activeClassName='active'
className='item' className='item'
exact={!hasParams} exact={isExact}
key={to} key={to}
strict={!hasParams} strict={isExact}
to={to} to={to}
> >
{text} {text}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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