// Copyright 2017-2019 @polkadot/react-components authors & contributors // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. import { BareProps } from './types'; import { KeyringOptions, KeyringSectionOption, KeyringSectionOptions, KeyringOption$Type } from '@polkadot/ui-keyring/options/types'; import React from 'react'; import store from 'store'; import styled from 'styled-components'; import createItem from '@polkadot/ui-keyring/options/item'; import keyring from '@polkadot/ui-keyring'; import keyringOption from '@polkadot/ui-keyring/options'; import { withMulti, withObservable } from '@polkadot/react-api'; import { classes, getAddressName } from './util'; import addressToAddress from './util/toAddress'; import Dropdown from './Dropdown'; interface Props extends BareProps { defaultValue?: string | null; help?: React.ReactNode; hideAddress?: boolean; isDisabled?: boolean; isError?: boolean; isInput?: boolean; isMultiple?: boolean; label?: string; labelExtra?: React.ReactNode; onChange?: (value: string | null) => void; onChangeMulti?: (value: string[]) => void; options?: KeyringSectionOption[]; optionsAll?: KeyringOptions; placeholder?: string; type?: KeyringOption$Type; value?: string | Uint8Array | string[]; withEllipsis?: boolean; withLabel?: boolean; } interface State { value?: string; } const STORAGE_KEY = 'options:InputAddress'; const DEFAULT_TYPE = 'all'; const transformToAddress = (value: string | Uint8Array): string | null => { try { return addressToAddress(value) || null; } catch (error) { console.error('Unable to transform address', value); } return null; }; const transformToAccountId = (value: string): string | null => { if (!value) { return null; } const accountId = transformToAddress(value); return !accountId ? null : accountId; }; const createOption = (address: string): KeyringSectionOption => { let isRecent: boolean | undefined; const pair = keyring.getAccount(address); let name: string | undefined; if (pair) { name = pair.meta.name; } else { const addr = keyring.getAddress(address); if (addr) { name = addr.meta.name; isRecent = addr.meta.isRecent; } else { isRecent = true; } } return createItem(address, name, !isRecent); }; class InputAddress extends React.PureComponent { public state: State = {}; public static getDerivedStateFromProps ({ value }: Props): Pick | null { try { return { value: Array.isArray(value) ? value.map(addressToAddress) : (addressToAddress(value) || undefined) }; } catch (error) { return null; } } public static readOptions (): Record { return store.get(STORAGE_KEY) || { defaults: {} }; } public static getLastValue (type: KeyringOption$Type = DEFAULT_TYPE): any { const options = InputAddress.readOptions(); return options.defaults[type]; } public static setLastValue (type: KeyringOption$Type = DEFAULT_TYPE, value: string): void { const options = InputAddress.readOptions(); options.defaults[type] = value; store.set(STORAGE_KEY, options); } public render (): React.ReactNode { const { className, defaultValue, help, hideAddress = false, isDisabled = false, isError, isMultiple, label, labelExtra, options, optionsAll, placeholder, type = DEFAULT_TYPE, style, withEllipsis, withLabel } = this.props; const { value } = this.state; const hasOptions = (options && options.length !== 0) || (optionsAll && Object.keys(optionsAll[type]).length !== 0); if (!hasOptions && !isDisabled) { return null; } const lastValue = InputAddress.getLastValue(type); const lastOption = this.getLastOptionValue(); const actualValue = transformToAddress( isDisabled || (defaultValue && this.hasValue(defaultValue)) ? defaultValue : ( this.hasValue(lastValue) ? lastValue : (lastOption && lastOption.value) ) ); const actualOptions = options || ( isDisabled && actualValue ? [createOption(actualValue)] : optionsAll ? optionsAll[type] : [] ); let _defaultValue; if (value !== undefined) { _defaultValue = undefined; } else if (isMultiple) { _defaultValue = undefined; } else { _defaultValue = actualValue; } return ( ); } private renderLabel = ({ value }: KeyringSectionOption): string | undefined => { if (!value) { return undefined; } return getAddressName(value, null, true); } private getLastOptionValue (): KeyringSectionOption | undefined { const { optionsAll, type = DEFAULT_TYPE } = this.props; if (!optionsAll) { return; } const available = optionsAll[type].filter(({ value }): boolean => !!value); return available.length ? available[available.length - 1] : undefined; } private hasValue (test?: string): boolean { const { optionsAll, type = DEFAULT_TYPE } = this.props; if (!optionsAll) { return false; } return !!optionsAll[type].find(({ value }): boolean => test === value); } private onChange = (address: string): void => { const { onChange, type } = this.props; InputAddress.setLastValue(type, address); onChange && onChange(transformToAccountId(address)); } private onChangeMulti = (addresses: string[]): void => { const { onChangeMulti } = this.props; if (onChangeMulti) { onChangeMulti( addresses .map(transformToAccountId) .filter((address): string => address as string) as string[] ); } } private onSearch = (filteredOptions: KeyringSectionOptions, _query: string): KeyringSectionOptions => { const { isInput = true } = this.props; const query = _query.trim(); const queryLower = query.toLowerCase(); const matches = filteredOptions.filter((item): boolean => item.value !== null && ( item.name.toLowerCase().indexOf(queryLower) !== -1 || item.value.toLowerCase().indexOf(queryLower) !== -1 ) ); const valueMatches = matches.filter((item): boolean => item.value !== null ); if (isInput && valueMatches.length === 0) { const accountId = transformToAccountId(query); if (accountId) { matches.push( keyring.saveRecent( accountId.toString() ).option ); } } return matches.filter((item, index): boolean => { const isLast = index === matches.length - 1; const nextItem = matches[index + 1]; const hasNext = nextItem && nextItem.value; return item.value !== null || (!isLast && !!hasNext); }); } } export { InputAddress }; export default withMulti( styled(InputAddress)` .ui.dropdown .text { width: 100%; } .ui.search.selection.dropdown { > .text > .ui--KeyPair { .ui--IdentityIcon { border: 1px solid #888; border-radius: 50%; left: -2.75rem; top: -1.2rem; } .name { margin-left: 0; } } } &.hideAddress .ui--KeyPair .address { flex: 0; max-width: 0; } `, withObservable(keyringOption.optionsSubject, { propName: 'optionsAll' }) );