Keyring observable subject around available options (#240)

* Start of keyring observable

* All update operations through observables

* Cleanup create

* onChange -> rxChange for observables

* Options is observable

* Fix swap bug with same params

* Missing onChange -> rxChange

* Cleanups

* Remove extra comment
This commit is contained in:
Jaco Greeff 2018-08-20 17:44:32 +02:00 committed by GitHub
parent 921621187c
commit bf4b3b4ed9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 513 additions and 291 deletions

View File

@ -8,6 +8,7 @@ import React from 'react';
import Button from '@polkadot/ui-app/Button';
import Input from '@polkadot/ui-app/Input';
import { InputAddress } from '@polkadot/ui-app/InputAddress';
import Password from '@polkadot/ui-app/Password';
import classes from '@polkadot/ui-app/util/classes';
import keyring from '@polkadot/ui-keyring/index';
@ -212,11 +213,12 @@ class Creator extends React.PureComponent<Props, State> {
onCommit = (): void => {
const { onBack } = this.props;
const { name, password, seed } = this.state;
keyring.createAccount(
const pair = keyring.createAccount(
formatSeed(seed), password, { name }
);
InputAddress.setLastValue('account', pair.address());
onBack();
}

View File

@ -8,6 +8,7 @@ import React from 'react';
import Button from '@polkadot/ui-app/Button';
import Input from '@polkadot/ui-app/Input';
import { InputAddress } from '@polkadot/ui-app/InputAddress';
import classes from '@polkadot/ui-app/util/classes';
import keyring from '@polkadot/ui-keyring/index';
import addressDecode from '@polkadot/util-keyring/address/decode';
@ -167,6 +168,7 @@ class Creator extends React.PureComponent<Props, State> {
const { address, name } = this.state;
keyring.saveAddress(address, { name });
InputAddress.setLastValue('address', address);
onBack();
}

View File

@ -2,7 +2,7 @@
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { KeyringOption$Type } from '@polkadot/ui-keyring/types';
import { KeyringOption$Type } from '@polkadot/ui-keyring/options/types';
import { I18nProps } from '@polkadot/ui-app/types';
import React from 'react';

View File

@ -13,13 +13,13 @@ import RxNonce from '@polkadot/ui-react-rx/Nonce';
type Props = BareProps & {
label: string,
onChange: (value: BN) => void,
rxChange: (value: BN) => void,
value?: Uint8Array
};
export default class Nonce extends React.PureComponent<Props> {
render () {
const { className, label, style, onChange, value } = this.props;
const { className, label, style, rxChange, value } = this.props;
if (!value) {
return null;
@ -36,7 +36,7 @@ export default class Nonce extends React.PureComponent<Props> {
>
<RxNonce
className='ui disabled dropdown selection'
onChange={onChange}
rxChange={rxChange}
params={value}
/>
</Labelled>

View File

@ -65,7 +65,7 @@ class Selection extends React.PureComponent<Props, State> {
label={t('display.nonce', {
defaultValue: 'with an index'
})}
onChange={this.onChangeNonce}
rxChange={this.onChangeNonce}
value={publicKey}
/>
<Button.Group>

View File

@ -79,7 +79,7 @@ class Account extends React.PureComponent<Props, State> {
>
<Nonce
className='ui disabled dropdown selection'
onChange={this.onChangeNonce}
rxChange={this.onChangeNonce}
params={publicKey}
/>
</Labelled>

View File

@ -59,6 +59,7 @@ class Selection extends React.PureComponent<Props, State> {
/>
<Params
item={key}
key={`${key.section}.${key.name}`}
onChange={this.onChangeParams}
/>
</div>
@ -107,7 +108,11 @@ class Selection extends React.PureComponent<Props, State> {
}
onChangeKey = (key: SectionItem<Storages>): void => {
this.nextState({ key } as State);
this.nextState({
isValid: false,
key,
params: []
});
}
onChangeParams = (params: RawParams = []): void => {

View File

@ -19,8 +19,6 @@ type Props = I18nProps & {};
const pkgJson = require('../package.json');
console.log('@polkadot/apps', pkgJson.version);
function updateTestInfo (chain?: string) {
keyring.setTestMode(isTestChain(chain));
}
@ -35,7 +33,7 @@ class NodeInfo extends React.PureComponent<Props> {
label={t('info.chain', {
defaultValue: 'chain: '
})}
onChange={updateTestInfo}
rxChange={updateTestInfo}
/>
<NodeName label={t('info.clientName', {
defaultValue: 'client: '

View File

@ -6,6 +6,7 @@ import { BareProps } from './types';
import React from 'react';
import SUIDropdown from 'semantic-ui-react/dist/commonjs/modules/Dropdown/Dropdown';
import isUndefined from '@polkadot/util/is/undefined';
import classes from './util/classes';
import Labelled from './Labelled';
@ -29,6 +30,17 @@ type SUIEvent = {
};
export default class Dropdown<Option> extends React.PureComponent<Props<Option>> {
componentDidMount () {
const { defaultValue, value } = this.props;
const startValue = isUndefined(value)
? defaultValue
: value;
if (startValue) {
this.onChange(null, { value: startValue });
}
}
render () {
const { className, defaultValue, isDisabled, isError, label, onSearch, options, placeholder, style, withLabel, value } = this.props;
@ -56,7 +68,7 @@ export default class Dropdown<Option> extends React.PureComponent<Props<Option>>
);
}
onChange = (event: React.SyntheticEvent<Element>, { value }: SUIEvent): void => {
onChange = (event: React.SyntheticEvent<Element> | null, { value }: SUIEvent): void => {
const { onChange, transform } = this.props;
onChange(

View File

@ -2,18 +2,20 @@
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { KeyringOptions, KeyringOption$Type } from '@polkadot/ui-keyring/types';
import { KeyringOptions, KeyringOptionsSection, KeyringOption$Type } from '@polkadot/ui-keyring/options/types';
import { BareProps } from '../types';
import './InputAddress.css';
import React from 'react';
import store from 'store';
import keyring from '@polkadot/ui-keyring/index';
import createOptionHeader from '@polkadot/ui-keyring/options/header';
import addressDecode from '@polkadot/util-keyring/address/decode';
import addressEncode from '@polkadot/util-keyring/address/encode';
import { optionsSubject } from '@polkadot/ui-keyring/options';
import makeOption from '@polkadot/ui-keyring/options/item';
import withObservableBase from '@polkadot/ui-react-rx/with/observableBase';
import isHex from '@polkadot/util/is/hex';
import hexToU8a from '@polkadot/util/hex/toU8a';
@ -22,13 +24,14 @@ import classes from '../util/classes';
import addressToAddress from '../util/toAddress';
type Props = BareProps & {
defaultValue?: string | Uint8Array | null,
defaultValue?: string | null,
hideAddress?: boolean;
isDisabled?: boolean,
isError?: boolean,
isInput?: boolean,
label?: string,
onChange: (value: Uint8Array) => void,
optionsAll?: KeyringOptions,
placeholder?: string,
type?: KeyringOption$Type,
value?: string | Uint8Array,
@ -36,12 +39,12 @@ type Props = BareProps & {
};
type State = {
defaultValue: string | undefined,
options: KeyringOptions,
value?: string
};
const RECENT_KEY = 'header-recent';
const STORAGE_KEY = 'options:InputAddress';
const DEFAULT_TYPE = 'all';
const transform = (value: string): Uint8Array => {
if (isHex(value)) {
@ -55,14 +58,11 @@ const transform = (value: string): Uint8Array => {
}
};
// NOTE: We are not extending Component here since the options may change in the keyring (which needs a re-render), however the input props will be the same (so, no PureComponent with shallow compare here)
export default class InputAddress extends React.Component<Props, State> {
class InputAddress extends React.PureComponent<Props, State> {
constructor (props: Props) {
super(props);
this.state = {
defaultValue: addressToAddress(props.defaultValue as string)
} as State;
this.state = {};
}
static getDerivedStateFromProps ({ value }: Props): State | null {
@ -75,13 +75,39 @@ export default class InputAddress extends React.Component<Props, State> {
}
}
static readOptions () {
return store.get(STORAGE_KEY) || { defaults: {} };
}
static getLastValue (type: KeyringOption$Type = DEFAULT_TYPE) {
const options = InputAddress.readOptions();
return options.defaults[type];
}
static setLastValue (type: KeyringOption$Type = DEFAULT_TYPE, value: string) {
const options = InputAddress.readOptions();
options.defaults[type] = value;
store.set(STORAGE_KEY, options);
}
render () {
const { className, hideAddress = false, isDisabled = false, isError, label, onChange, type = 'all', style, withLabel } = this.props;
const { defaultValue, value } = this.state;
const options = keyring.getOptions(type);
const hasValue = !!options.find(({ key }) =>
key === defaultValue
);
const { className, defaultValue, hideAddress = false, isDisabled = false, isError, label, optionsAll, type = DEFAULT_TYPE, style, withLabel } = this.props;
const { value } = this.state;
if (!optionsAll) {
return null;
}
const lastValue = InputAddress.getLastValue(type);
const actualValue = isDisabled || (defaultValue && this.hasValue(defaultValue))
? defaultValue
: (
this.hasValue(lastValue)
? lastValue
: undefined
);
return (
<Dropdown
@ -89,42 +115,57 @@ export default class InputAddress extends React.Component<Props, State> {
defaultValue={
value !== undefined
? undefined
: defaultValue
: actualValue
}
isDisabled={isDisabled}
isError={isError}
label={label}
onChange={onChange}
onChange={this.onChange}
onSearch={this.onSearch}
options={
isDisabled && !hasValue && defaultValue
? [makeOption(defaultValue)]
: options
isDisabled && actualValue
? [makeOption(actualValue)]
: optionsAll[type]
}
style={style}
transform={transform}
value={value}
withLabel={withLabel}
/>
);
}
onSearch = (filteredOptions: KeyringOptions, query: string): KeyringOptions => {
private hasValue (value?: string): boolean {
const { optionsAll, type = DEFAULT_TYPE } = this.props;
if (!optionsAll) {
return false;
}
return !!optionsAll[type].find(({ key }) =>
key === value
);
}
onChange = (address: string) => {
const { onChange, type } = this.props;
InputAddress.setLastValue(type, address);
onChange(transform(address));
}
onSearch = (filteredOptions: KeyringOptionsSection, query: string): KeyringOptionsSection => {
const { isInput = true } = this.props;
const queryLower = query.toLowerCase();
const matches = filteredOptions.filter((item) => {
if (item.value === null) {
return true;
}
const matches = filteredOptions.filter((item) =>
item.value === null ||
item.name.toLowerCase().indexOf(queryLower) !== -1 ||
item.value.toLowerCase().indexOf(queryLower) !== -1
);
const { name, value } = item;
const hasMatch = name.toLowerCase().indexOf(queryLower) !== -1 ||
value.toLowerCase().indexOf(queryLower) !== -1;
return hasMatch;
});
const valueMatches = matches.filter((item) => item.value !== null);
const valueMatches = matches.filter((item) =>
item.value !== null
);
if (isInput && valueMatches.length === 0) {
const publicKey = transform(query);
@ -139,7 +180,7 @@ export default class InputAddress extends React.Component<Props, State> {
matches.push(
keyring.saveRecent(
addressEncode(publicKey)
)
).option
);
}
}
@ -149,11 +190,12 @@ export default class InputAddress extends React.Component<Props, State> {
const nextItem = matches[index + 1];
const hasNext = nextItem && nextItem.value;
if (item.value !== null || (!isLast && hasNext)) {
return true;
}
return false;
return item.value !== null || (!isLast && hasNext);
});
}
}
export { InputAddress };
// @ts-ignore There are still some issues with props and types - this is valid
export default withObservableBase(optionsSubject, { propName: 'optionsAll' })(InputAddress);

View File

@ -7,10 +7,14 @@ import { KeyringAddress, State } from '../types';
import get from '../address/get';
export default function all (state: State): Array<KeyringAddress> {
const available = state.accounts.subject.getValue();
return Object
.keys(state.available.account)
.keys(available)
.map((address) =>
get(state, address, 'account')
)
.filter((account) => !account.getMeta().isTesting);
.filter((account) =>
!account.getMeta().isTesting
);
}

View File

@ -4,17 +4,7 @@
import { State } from '../types';
import store from 'store';
import { accountKey } from '../defaults';
import createOptions from '../options';
export default function forgetAccount (state: State, address: string): void {
state.keyring.removePair(address);
store.remove(accountKey(address));
delete state.available.account[address];
createOptions(state);
state.accounts.remove(address);
}

View File

@ -8,7 +8,6 @@ import { State } from '../types';
import store from 'store';
import { accountKey } from '../defaults';
import createOptions from '../options';
export default function saveAccountMeta (state: State, pair: KeyringPair, meta: KeyringPair$Meta): void {
const address = pair.address();
@ -17,7 +16,5 @@ export default function saveAccountMeta (state: State, pair: KeyringPair, meta:
pair.setMeta(meta);
json.meta = pair.getMeta();
store.set(accountKey(json.address), json);
createOptions(state);
state.accounts.add(json.address, json);
}

View File

@ -5,11 +5,6 @@
import { KeyringPair } from '@polkadot/util-keyring/types';
import { State } from '../types';
import store from 'store';
import { accountKey } from '../defaults';
import createOptions from '../options';
export default function saveAccount (state: State, pair: KeyringPair, password?: string): void {
const json = pair.toJson(password);
@ -17,7 +12,5 @@ export default function saveAccount (state: State, pair: KeyringPair, password?:
json.meta.whenCreated = Date.now();
}
store.set(accountKey(json.address), json);
createOptions(state);
state.accounts.add(json.address, json);
}

View File

@ -7,8 +7,10 @@ import { KeyringAddress, State } from '../types';
import get from './get';
export default function all (state: State): Array<KeyringAddress> {
const available = state.addresses.subject.getValue();
return Object
.keys(state.available.address)
.keys(available)
.map((address) =>
get(state, address)
);

View File

@ -4,15 +4,6 @@
import { State } from '../types';
import store from 'store';
import { addressKey } from '../defaults';
import createOptions from '../options';
export default function forgetAddress (state: State, address: string): void {
store.remove(addressKey(address));
delete state.available.address[address];
createOptions(state);
state.addresses.remove(address);
}

View File

@ -13,15 +13,18 @@ export default function get (state: State, _address: string | Uint8Array, type:
? _address
: addressEncode(_address);
const publicKey = addressDecode(address);
const subject = type === 'account'
? state.accounts.subject
: state.addresses.subject;
return {
address: (): string =>
address,
isValid: (): boolean =>
!!state.available[type][address],
!!subject.getValue()[address],
publicKey: (): Uint8Array =>
publicKey,
getMeta: (): KeyringJson$Meta =>
state.available[type][address].meta
subject.getValue()[address].json.meta
};
}

View File

@ -5,13 +5,10 @@
import { KeyringPair$Meta } from '@polkadot/util-keyring/types';
import { State } from '../types';
import store from 'store';
import { addressKey } from '../defaults';
import createOptions from '../options';
export default function saveMeta (state: State, address: string, meta: KeyringPair$Meta): void {
const json = state.available.address[address] || {
const available = state.addresses.subject.getValue();
const json = (available[address] && available[address].json) || {
address,
meta: {
isRecent: void 0,
@ -25,8 +22,5 @@ export default function saveMeta (state: State, address: string, meta: KeyringPa
delete json.meta.isRecent;
store.set(addressKey(address), json);
state.available.address[address] = json;
createOptions(state);
state.addresses.add(address, json);
}

View File

@ -2,15 +2,13 @@
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { KeyringJson, KeyringOption, State } from '../types';
import { SingleAddress } from '../observable/types';
import { KeyringJson, State } from '../types';
import store from 'store';
export default function saveRecent (state: State, address: string): SingleAddress {
const available = state.addresses.subject.getValue();
import { addressKey } from '../defaults';
import createOptions from '../options';
export default function saveRecent (state: State, address: string): KeyringOption {
if (!state.available.address[address]) {
if (!available[address]) {
const json = {
address,
meta: {
@ -19,12 +17,8 @@ export default function saveRecent (state: State, address: string): KeyringOptio
}
};
store.set(addressKey(json.address), json);
state.available.address[address] = (json as KeyringJson);
state.addresses.add(address, (json as KeyringJson));
}
createOptions(state);
// @ts-ignore it should be there now...
return state.options.recent.find(({ value }) => value === address);
return state.addresses.subject.getValue()[address];
}

View File

@ -3,10 +3,13 @@
// of the ISC license. See the LICENSE file for details.
import { KeyringPair, KeyringPair$Meta } from '@polkadot/util-keyring/types';
import { KeyringAddress, KeyringInstance, KeyringOption$Type, KeyringOption, KeyringOptions, State } from './types';
import { SingleAddress } from './observable/types';
import { KeyringAddress, KeyringInstance, State } from './types';
import testKeyring from '@polkadot/util-keyring/testing';
import accounts from './observable/accounts';
import addresses from './observable/addresses';
import loadAll from './loadAll';
import createAccount from './account/create';
import forgetAccount from './account/forget';
@ -23,12 +26,9 @@ import setTestMode from './setTestMode';
const state: State = {
isTestMode: false,
available: {
account: {},
address: {}
},
keyring: testKeyring(),
options: {}
accounts,
addresses,
keyring: testKeyring()
};
loadAll(state);
@ -48,8 +48,6 @@ export default ({
getAddress(state, address),
getAddresses: (): Array<KeyringAddress> =>
getAddresses(state),
getOptions: (type: KeyringOption$Type): KeyringOptions =>
state.options[type],
getPair: (address: string | Uint8Array): KeyringPair =>
state.keyring.getPair(address),
getPairs: (): Array<KeyringPair> =>
@ -64,7 +62,7 @@ export default ({
saveAccountMeta(state, pair, meta),
saveAddress: (address: string, meta: KeyringPair$Meta): void =>
saveAddress(state, address, meta),
saveRecent: (address: string): KeyringOption =>
saveRecent: (address: string): SingleAddress =>
saveRecent(state, address),
setTestMode: (isTest: boolean): void =>
setTestMode(state, isTest)

View File

@ -8,9 +8,12 @@ import isString from '@polkadot/util/is/string';
import addressEncode from '@polkadot/util-keyring/address/encode';
export default function isAvailable (state: State, _address: Uint8Array | string): boolean {
const accounts = state.accounts.subject.getValue();
const addresses = state.addresses.subject.getValue();
const address = isString(_address)
? _address
: addressEncode(_address);
return !state.available.account[address] && !state.available.address[address];
return !accounts[address] && !addresses[address];
}

View File

@ -11,16 +11,31 @@ import isHex from '@polkadot/util/is/hex';
import hexToU8a from '@polkadot/util/hex/toU8a';
import saveAddress from './address/meta';
import createOptions from './options';
import initOptions from './options';
import { accountRegex, addressRegex } from './defaults';
function addPairs ({ accounts, keyring }: State): void {
keyring
.getPairs()
.forEach((pair) => {
const address = pair.address();
accounts.add(address, {
address,
meta: pair.getMeta()
});
});
}
export default function loadAll (state: State): void {
const { available, keyring } = state;
const { accounts, addresses, keyring } = state;
addPairs(state);
store.each((json: KeyringJson, key: string) => {
if (accountRegex.test(key)) {
keyring.addFromJson(json as KeyringPair$Json);
available.account[json.address] = json;
accounts.add(json.address, json);
} else if (addressRegex.test(key)) {
const address = isHex(json.address)
? addressEncode(hexToU8a(json.address))
@ -35,9 +50,9 @@ export default function loadAll (state: State): void {
saveAddress(state, address, json.meta);
}
available.address[json.address] = json;
addresses.add(json.address, json);
}
});
createOptions(state);
initOptions(state);
}

View File

@ -0,0 +1,9 @@
// Copyright 2017-2018 @polkadot/ui-keyring authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { accountKey } from '../defaults';
import genericSubject from './genericSubject';
export default genericSubject(accountKey);

View File

@ -0,0 +1,9 @@
// Copyright 2017-2018 @polkadot/ui-keyring authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { addressKey } from '../defaults';
import genericSubject from './genericSubject';
export default genericSubject(addressKey);

View File

@ -0,0 +1,41 @@
// Copyright 2017-2018 @polkadot/ui-keyring authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { BehaviorSubject } from 'rxjs';
import { SubjectInfo, AddressSubject, SingleAddress } from './types';
import { KeyringJson } from '../types';
import store from 'store';
import createOptionItem from '../options/item';
export default function genericSubject (keyCreator: (address: string) => string): AddressSubject {
let current: SubjectInfo = {};
const subject = new BehaviorSubject({});
return {
add: (address: string, json: KeyringJson): SingleAddress => {
current = { ...current };
current[address] = {
json,
option: createOptionItem(address, json.meta.name)
};
store.set(keyCreator(address), json);
subject.next(current);
return current[address];
},
remove: (address: string) => {
current = { ...current };
delete current[address];
store.remove(keyCreator(address));
subject.next(current);
},
subject
};
}

View File

@ -0,0 +1,19 @@
// Copyright 2017-2018 @polkadot/ui-keyring authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
import accounts from './accounts';
import addresses from './addresses';
export default combineLatest(
accounts.subject,
addresses.subject
).pipe(
map(([accounts, addresses]) => ({
accounts,
addresses
}))
);

View File

@ -0,0 +1,22 @@
// Copyright 2017-2018 @polkadot/ui-keyring authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { BehaviorSubject } from 'rxjs';
import { KeyringSectionOption } from '../options/types';
import { KeyringJson } from '../types';
export type SingleAddress = {
json: KeyringJson,
option: KeyringSectionOption
}
export type SubjectInfo = {
[index: string]: SingleAddress
};
export type AddressSubject = {
add: (address: string, json: KeyringJson) => SingleAddress,
remove: (address: string) => void,
subject: BehaviorSubject<SubjectInfo>
};

View File

@ -2,9 +2,9 @@
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { KeyringOption } from '../types';
import { KeyringSectionOption } from './types';
export default function createHeader (name: string): KeyringOption {
export default function createHeader (name: string): KeyringSectionOption {
return {
className: 'header disabled',
name,

View File

@ -2,31 +2,24 @@
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { State, KeyringJson, KeyringOptions } from '../types';
import { State } from '../types';
import { SingleAddress } from '../observable/types';
import { KeyringOptions, KeyringOptionsSection } from './types';
import createItem from './item';
import { BehaviorSubject } from 'rxjs';
import observableAll from '../observable';
import createHeader from './header';
function addPairs ({ available, keyring }: State): void {
keyring
.getPairs()
.forEach((pair) => {
const address = pair.address();
function addAccounts ({ accounts, isTestMode }: State, options: KeyringOptions): void {
const available = accounts.subject.getValue();
available.account[address] = {
address,
meta: pair.getMeta()
};
});
}
function addAccounts ({ available, isTestMode, options }: State): void {
Object
.keys(available.account)
.map((address) => available.account[address])
.forEach(({ address, meta: { name, isTesting = false } }: KeyringJson) => {
const option = createItem(address, name);
.keys(available)
.map((address) =>
available[address]
)
.forEach(({ json: { meta: { isTesting = false } }, option }: SingleAddress) => {
if (!isTesting) {
options.account.push(option);
} else if (isTestMode) {
@ -35,17 +28,15 @@ function addAccounts ({ available, isTestMode, options }: State): void {
});
}
function addAddresses ({ available, options }: State): void {
function addAddresses ({ addresses }: State, options: KeyringOptions): void {
const available = addresses.subject.getValue();
Object
.keys(available.address)
.map((address) => available.address[address])
.forEach(({ address, meta: { name, isRecent = false } }: KeyringJson) => {
if (available.account[address]) {
return;
}
const option = createItem(address, name);
.keys(available)
.map((address) =>
available[address]
)
.forEach(({ json: { meta: { isRecent = false } }, option }: SingleAddress) => {
if (isRecent) {
options.recent.push(option);
} else {
@ -54,36 +45,48 @@ function addAddresses ({ available, options }: State): void {
});
}
export default function createOptions (state: State): void {
state.options = {
function emptyOptions (): KeyringOptions {
return {
account: [],
address: [],
all: [],
recent: [],
testing: []
};
addPairs(state);
addAccounts(state);
addAddresses(state);
const { options } = state;
options.address = ([] as KeyringOptions).concat(
options.address.length ? [ createHeader('Addresses') ] : [],
options.address,
options.recent.length ? [ createHeader('Recent') ] : [],
options.recent
);
options.account = ([] as KeyringOptions).concat(
options.account.length ? [ createHeader('Accounts') ] : [],
options.account,
options.testing.length ? [ createHeader('Testing') ] : [],
options.testing
);
options.all = ([] as KeyringOptions).concat(
options.account,
options.address
);
}
const optionsSubject: BehaviorSubject<KeyringOptions> = new BehaviorSubject(emptyOptions());
// NOTE To be called _only_ once (should be addressed with https://github.com/polkadot-js/apps/issues/138)
export default function initOptions (state: State): void {
observableAll.subscribe((value) => {
const options = emptyOptions();
addAccounts(state, options);
addAddresses(state, options);
options.address = ([] as KeyringOptionsSection).concat(
options.address.length ? [ createHeader('Addresses') ] : [],
options.address,
options.recent.length ? [ createHeader('Recent') ] : [],
options.recent
);
options.account = ([] as KeyringOptionsSection).concat(
options.account.length ? [ createHeader('Accounts') ] : [],
options.account,
options.testing.length ? [ createHeader('Testing') ] : [],
options.testing
);
options.all = ([] as KeyringOptionsSection).concat(
options.account,
options.address
);
optionsSubject.next(options);
});
}
export {
optionsSubject
};

View File

@ -2,15 +2,15 @@
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import { KeyringOption } from '../types';
import { KeyringSectionOption } from './types';
import React from 'react';
import KeyPair from './KeyPair';
export default function createItem (address: string, _name?: string): KeyringOption {
export default function createItem (address: string, _name?: string): KeyringSectionOption {
const name = _name === undefined
? `${address.slice(0, 16)}…`
? `${address.slice(0, 7)}…${address.slice(-7)}`
: _name;
return {

View File

@ -0,0 +1,25 @@
// Copyright 2017-2018 @polkadot/ui-keyring authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
export type KeyringSectionOption = {
className?: string,
disabled?: boolean,
content?: any | string, // node?
key: string | null,
name: string,
text: any | string, // node?
value: string | null
};
export type KeyringOptionsSection = Array<KeyringSectionOption>;
export type KeyringOptions = {
account: KeyringOptionsSection,
address: KeyringOptionsSection,
all: KeyringOptionsSection,
recent: KeyringOptionsSection,
testing: KeyringOptionsSection
}
export type KeyringOption$Type = keyof KeyringOptions;

View File

@ -4,10 +4,6 @@
import { State } from './types';
import createOptions from './options';
export default function setTestMode (state: State, isTest: boolean): void {
state.isTestMode = isTest;
createOptions(state);
}

View File

@ -3,20 +3,8 @@
// of the ISC license. See the LICENSE file for details.
import { KeyringInstance as BaseKeyringInstance, KeyringPair, KeyringPair$Meta } from '@polkadot/util-keyring/types';
export type KeyringOption$Type = 'account' | 'address' | 'all' | 'recent' | 'testing';
export type KeyringOption = {
className?: string,
disabled?: boolean,
content?: any | string, // node?
key: string | null,
name: string,
text: any | string, // node?
value: string | null
};
export type KeyringOptions = Array<KeyringOption>;
import { AddressSubject, SingleAddress } from './observable/types';
import { KeyringSectionOption } from './options/types';
export type KeyringJson$Meta = {
isRecent?: boolean,
@ -42,18 +30,9 @@ export type KeyringAddress = {
export type State = {
isTestMode: boolean,
available: {
account: {
[index: string]: KeyringJson
},
address: {
[index: string]: KeyringJson
}
},
keyring: BaseKeyringInstance,
options: {
[index: string]: KeyringOptions // KeyringOption$Type
}
accounts: AddressSubject,
addresses: AddressSubject,
keyring: BaseKeyringInstance
};
export type KeyringInstance = {
@ -63,7 +42,6 @@ export type KeyringInstance = {
getAccounts: () => Array<KeyringAddress>,
getAddress: (address: string | Uint8Array) => KeyringAddress,
getAddresses: () => Array<KeyringAddress>,
getOptions: (type: KeyringOption$Type) => KeyringOptions,
getPair: (address: string | Uint8Array) => KeyringPair,
getPairs: () => Array<KeyringPair>,
isAvailable: (address: string | Uint8Array) => boolean,
@ -71,6 +49,6 @@ export type KeyringInstance = {
saveAccount: (pair: KeyringPair, password?: string) => void,
saveAccountMeta: (pair: KeyringPair, meta: KeyringPair$Meta) => void,
saveAddress: (address: string, meta: KeyringPair$Meta) => void,
saveRecent: (address: string) => KeyringOption,
saveRecent: (address: string) => SingleAddress,
setTestMode: (isTest: boolean) => void
};

View File

@ -36,7 +36,7 @@ type OnChangeCb$Fn<T> = (value?: T) => void;
export type OnChangeCb<T> = OnChangeCb$Obs<T> | OnChangeCb$Fn<T> | undefined;
export type ChangeProps<T> = {
onChange?: OnChangeCb<T>
rxChange?: OnChangeCb<T>
};
export type ParamProps = {

View File

@ -2,6 +2,24 @@
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
export default function isEqual <T> (a?: T, b?: T): boolean {
return JSON.stringify({ test: a }) === JSON.stringify({ test: b });
function flatten (key: string | null, value: any): any {
if (!value) {
return value;
}
if (value.$$typeof) {
return '';
}
if (Array.isArray(value)) {
return value.map((item) =>
flatten(null, item)
);
}
return value;
}
export default function isEqual <T> (a?: T, b?: T): boolean {
return JSON.stringify({ test: a }, flatten) === JSON.stringify({ test: b }, flatten);
}

View File

@ -7,16 +7,16 @@ import { OnChangeCb, OnChangeCb$Obs } from '../types';
import isFunction from '@polkadot/util/is/function';
import isObservable from '@polkadot/util/is/observable';
export default function triggerChange<T> (value?: T, ...onChange: Array<OnChangeCb<T>>): void {
if (!onChange || !onChange.length) {
export default function triggerChange<T> (value?: T, ...rxChange: Array<OnChangeCb<T>>): void {
if (!rxChange || !rxChange.length) {
return;
}
onChange.forEach((onChange) => {
if (isObservable(onChange)) {
(onChange as OnChangeCb$Obs<T>).next(value);
} else if (isFunction(onChange)) {
onChange(value);
rxChange.forEach((rxChange) => {
if (isObservable(rxChange)) {
(rxChange as OnChangeCb$Obs<T>).next(value);
} else if (isFunction(rxChange)) {
rxChange(value);
}
});
}

View File

@ -9,22 +9,20 @@ import { RxProps } from '../types';
import { HOC, Options, DefaultProps, RenderFn } from './types';
import React from 'react';
import { map } from 'rxjs/operators/map';
import isUndefined from '@polkadot/util/is/undefined';
import intervalSubscribe from '../util/intervalSubscribe';
import isEqual from '../util/isEqual';
import triggerChange from '../util/triggerChange';
import echoTransform from './transform/echo';
import withApi from './api';
import withObservableBase from './observableBase';
type State<T> = RxProps<T> & {
subscriptions: Array<any>; // FIXME subscriptions
Component?: React.ComponentType<any>
};
// FIXME proper types for attributes
export default function withObservable<T> (observable: ObservableApiNames, { onChange, params = [], paramProp = 'params', propName = observable, transform = echoTransform }: Options<T> = {}): HOC<T> {
export default function withObservable<T> (subscription: ObservableApiNames, { rxChange, params = [], paramProp = 'params', propName = subscription, transform = echoTransform }: Options<T> = {}): HOC<T> {
return (Inner: React.ComponentType<any>, defaultProps: DefaultProps<T> = {}, render?: RenderFn): React.ComponentType<any> => {
class WithObservable extends React.Component<any, State<T>> {
state: State<T>;
@ -32,12 +30,7 @@ export default function withObservable<T> (observable: ObservableApiNames, { onC
constructor (props: any) {
super(props);
this.state = {
rxUpdated: false,
rxUpdatedAt: 0,
subscriptions: [],
value: void 0
};
this.state = {};
}
private getParams (props: any): Array<any> {
@ -64,63 +57,34 @@ export default function withObservable<T> (observable: ObservableApiNames, { onC
this.subscribe(this.getParams(this.props));
}
private subscribe (newParams: Array<any>) {
const { apiObservable } = this.props;
this.unsubscribe();
componentWillUnmount () {
this.setState({
subscriptions: [
apiObservable[observable](...newParams)
.pipe(map(transform))
.subscribe((value: any) =>
this.triggerUpdate(this.props, value)
),
intervalSubscribe(this)
]
Component: undefined
});
}
private unsubscribe () {
this.state.subscriptions.forEach((subscription) =>
subscription.unsubscribe()
);
}
componentWillUnmount () {
this.unsubscribe();
}
triggerUpdate = (props: any, value?: T): void => {
if (isEqual(value, this.state.value)) {
return;
}
triggerChange(value, onChange, props.onChange || defaultProps.onChange);
private subscribe (newParams: Array<any>) {
const { apiObservable } = this.props;
const observable = apiObservable[subscription](...newParams);
this.setState({
rxUpdated: true,
rxUpdatedAt: Date.now(),
value
Component: withObservableBase(observable, {
rxChange,
propName,
transform
})(Inner, defaultProps, render)
});
}
render () {
const { children } = this.props;
const { rxUpdated, rxUpdatedAt, value } = this.state;
const _props = {
...defaultProps,
...this.props,
rxUpdated,
rxUpdatedAt,
[propName]: value
};
const { Component } = this.state;
delete _props.onChange;
if (!Component) {
return null;
}
return (
<Inner {..._props}>
{render && render(value)}{children}
</Inner>
<Component {...this.props} />
);
}
}

View File

@ -0,0 +1,93 @@
// Copyright 2017-2018 @polkadot/ui-react-rx authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
// TODO: Lots of duplicated code between this and withObservable, surely there ois a better way of doing this?
import { RxProps } from '../types';
import { HOC, Options, DefaultProps, RenderFn } from './types';
import React from 'react';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import intervalSubscribe from '../util/intervalSubscribe';
import isEqual from '../util/isEqual';
import triggerChange from '../util/triggerChange';
import echoTransform from './transform/echo';
type State<T> = RxProps<T> & {
subscriptions: Array<any>; // FIXME subscriptions
};
// FIXME proper types for attributes
export default function withObservableBase<T> (observable: Observable<any>, { rxChange, propName = 'value', transform = echoTransform }: Options<T> = {}): HOC<T> {
return (Inner: React.ComponentType<any>, defaultProps: DefaultProps<T> = {}, render?: RenderFn): React.ComponentType<any> => {
return class WithObservable extends React.Component<any, State<T>> {
state: State<T>;
constructor (props: any) {
super(props);
this.state = {
rxUpdated: false,
rxUpdatedAt: 0,
subscriptions: [],
value: void 0
};
}
componentDidMount () {
this.setState({
subscriptions: [
observable
.pipe(map(transform))
.subscribe((value: any) =>
this.triggerUpdate(this.props, value)
),
intervalSubscribe(this)
]
});
}
componentWillUnmount () {
this.state.subscriptions.forEach((subscription) =>
subscription.unsubscribe()
);
}
triggerUpdate = (props: any, value?: T): void => {
if (isEqual(value, this.state.value)) {
return;
}
triggerChange(value, rxChange, props.rxChange || defaultProps.rxChange);
this.setState({
rxUpdated: true,
rxUpdatedAt: Date.now(),
value
});
}
render () {
const { children } = this.props;
const { rxUpdated, rxUpdatedAt, value } = this.state;
const _props = {
...defaultProps,
...this.props,
rxUpdated,
rxUpdatedAt,
[propName]: value
};
return (
<Inner {..._props}>
{render && render(value)}{children}
</Inner>
);
}
};
};
}

View File

@ -11,12 +11,12 @@ import React from 'react';
export type Transform = (value: any, index: number) => any;
export type DefaultProps<T> = {
onChange?: OnChangeCb<T>,
rxChange?: OnChangeCb<T>,
[index: string]: any
};
export type Options<T> = {
onChange?: OnChangeCb<T>,
rxChange?: OnChangeCb<T>,
params?: Array<any>,
paramProp?: string,
propName?: string,