* fix: Fixes Issue #144. Balance to submit with transaction changed to string Placeholder indicates acceptable range of values to user in the interim until form validation incorporated. Max value must be the balance of the account minus one, otherwise the Polkadot Node will give error `Runtime: proposer's balance too low`. Min value must be above 0.000000000000001, otherwise the Polkadot Node will give error `Runtime: proposer's balance too low` * fix: Add validations to string input. Add unit tests Note that entering a large number after all inputs are valid (including code dragged and dropped) causes error `Uncaught Error: byte array longer than desired length`. This is because it only starts encoding after all input fields are valid, but the maximum balance is 64 bits for poc-1 and 128 bits for latest master branch * fix: Add internationalisation, maxLength to input, validate depending on chain used - add internationalisation - add maxLength to input field depending on whether poc-1 (64bit) or latest (128bit) chain used - pass down api chain data via props - add unit tests for balance input field, new tests not all passing but don't understand why. since using unsigned 64bit and 128bit - add balance input field validation to only accept numbers - when i add `&& maxBN64Bit.gte(inputBN)` or `&& maxBN128Bit.gte(inputBN)` it doesn't appear to work correctly as in the UI it doesn't allow a length as much as expected * fix: Must pass in as string as String(xyz) converts to scientific notation x...e+21 for large numbers * fix: Remove String(xyz) that was converting to scientific notation. Throw on it or non-string input. * fix: Prop value is BN converted to string with toString(10) according to BN.js docs * fix: Translate the placeholder * refactor: Move external dependencies above local. Add missing whitespace * fix: Scientific value detection on e+ not just letter e * feat: Add input field validation message notification to extrinsic balance * refactor: Move expected responses generation into enum in separate file * WIP - trying with input type number and onKeyDown * WIP - split into onKeyDown and onKeyUp * feat: Add scientific notation equivalent in info message. Yet to use converted value when submit * fix: Allow user to select all that they've entered in input field * feat: Converts from scientific notation with submit. Need to fix input as crashes on 9e19 * fix: Remove debugging console.log so it doesn't crash * fix: Calculate size based on actual bitlength before checking BN value against max * fix: Allow use of decimal point only in scientific notation. Prevent successive decimal point entry * fix: Update error message with example * feat: Create InputNumber instead of extending Balance. Requires PR with mods to polkadot-js/apps * fix: Remove accidently added vscode config * fix: Remove step and min props since not using input type number * refactor: Remove debugging console logs from extrinsics * fix: Remove disused step * fix: Remove comments and add whitespace * fix: Remove unnecessary check if balance is defined when it must be * fix: Fix logic * fix: Show error when user enters e or e+ without a number afterwards * test: Update existing tests. Still must add new tests * test: Fix tests for scientific vs exponential notation * test: Add remaining tests * test: Refactor tests in order used * refactor: clean up comments and duplicate code * fix: Replace implementation with restrictions on key combos instead of horrible previous approach * fix: Prevent notification with info from displaying when there are errors. Remove console logs * fix: Prevent user pasting balance in exponential notation with invalid exponent. Only allow + symbol entry immediately after letter e - Only allow user to enter + symbol immediately after letter e for exponential input. - Prevent error when user pastes input with invalid exponent (i.e. when user pastes into the input field for Balance 232e11+ it was crashing with error `error: RangeError: Invalid array length at scientificToDecimal`) * refactor: Add comment to input cursor and move * remove todo note * fix: Prevent user entering 0's at start * restore: Restore Balance instead of incorrect implementation of InputNumber * WIP - initially add generic identity function where Balance wraps InputNumber type * remove generic ident fn * feat: Add GenericInputNumber * fix: Add InputNumber generic wrapper of Input and Notification for use in Balance or elsewhere * fix: Address review comments. * fix: Fix linting errors and remove useless new line * fix: Add WarnMessage. Remove UI message for it to be account balance minus 1 * fix: Fix bitLength error when small amount entered. Fix NaN test. Allow input of 0 with warning. * fix: Allow select all, cut, copy, paste whilst preventing input of a, x, c, v * fix: Fix linting error that appeared in CI * fix: Remove conflicting restriction on use of 0 * test: Add enzyme test for default load of Balance component * test: Add basic enzyme tests to simulate Info, Warning, and Error Notifications * fix: Address some review comments. See commit details * Refactor nested condition into `isValidLength` instead of `(values[index].type === 'Balance' ? checkValueBitLength(...` * Fix alphabetical ordering * Change to `Between 0 DOT` instead of `Between 1 DOT` * Add License heaer to chainSpec.ts * Replace `if (!(typeof input === 'string')) ` with `isString` * Add blank lines for all blocks in isValidBalanceExpectedResponses.ts * fix: Remove NaN check since unnecessary. Replace with test for just single e input * Remove check for NaN since unnecessary. NaN is not string type so will show error associated with the test for string * Add initial check for input value that contains just 'e' since it should not show same error message as say '1e' * fix: Revert to just allow integers and decimals without scientific notation * docs: fix comments * refactor: Split isValid ternary across multiple lines for readability * refactor: Remove unnecessary blank line * fix: Remove nested if condition * refactor: Split isCopy function definiton from function body implementation similar to in ui-signer * refactor: Split function definition from function body for all keyboard utils * fix: Replace event any type with KeyboardEvent type for all keyboard functions * fix: Remove TranslateFunction with just string as type of messages * fix: Replace deprecated keyCode and which with key. * fix: Move isValidLength into isValidBalance * fix: Split imported packages * fix: Split imported packages in Balance spec * fix: Change placeholder to request positive integer input and update messages * refactor: Split function body onto new line * fix: Split function body onto new line for readability * refactor: Split ternary across multiple lines * fix: Use BN instead of Number for info message * fix: pass up destructured event properties * fix: Remove SUIEvent type definition * fix: Reduce maxLength in digits that user allowed to enter by 1 * merge latest from master * fix: Remove transfer terminology from Balance placeholder * Move logic to child * fix: Remove assignment to optional * fix: Remove DOT references since number can be anything and substrate chains have own token * fix: Add missing blank lines and reorder alphabetically * Non-numeric keys outright without error notification. Use MAX_SAFE_INTEGER built-in * fix: Reordering * fix: Remove showing error to user when they enter value over max bitlength * fix: Restrict user enter value greater than max safe integer * fix: Do not pass translate down from Balance. Fix tests * test: Move tests to InputNumber * Return only value from onChange in InputNumber. Bug since isValid keydowns allowed * fix: Fixes review comments. See commit message details: * `onChange` in InputNumber is just `onChange: (value: BN) => void` without an `isValid` property. `isValid` is now set to `true` in Balance as we've taken care of all the invalid cases (i.e. automatically reverting the input if the user pastes an invalid value, or preventing input of non-decimal values in keyDown) * New input value is reverted to previous valid input field value if the input field value becames invalid (i.e. if they enter a value that's above the maximum allowable). i.e. 0.1111111111111111111111111111111 is allowed, but if any additional 1's are entered (i.e. 0.1111111111111111111111111111111111111) at the end it exceeds max for 128 bit, and we automatically prevent these invalid values from being entered (instead of informing the user through a notification). * Refer to input as "input" instead of "balance" (i.e. for i18n keys, etc) * Remove duplicate `KEYS.BACKSPACE` * Add unit tests for isNonDecimal and isNonInteger * Remove `KEYS.ZERO` from `KEYS_ALLOWED` and add to isValidDecimal and isValidInteger checks instead * Split isValidNumber into isValidNumber and isValidKey. Since keyDown has access to the key and can prevent the input (but only has access to the previous input field value before the new key was entered), we check if the the new key entered is valid in isolation of the whole valued by calling isValidKey. Then in onChange where we have access to the new input field value we validate it calling isValidNumber. * In InputNumber.tsx the Input component accepts `defaultValue={defaultValue || '0'}`, and also we're also overriding the value with the previous valid value with `value={revertedValue}` if the user input is found to be invalid. However React complains with the following errors in the browser Chrome Inspector: ``` Warning: Input contains an input of type text with both value and defaultValue props. Input elements must be either controlled or uncontrolled (specify either the value prop, or the defaultValue prop, but not both). Decide between using a controlled or uncontrolled input element and remove one of these props. More info: https://fb.me/react-controlled-components ``` ``` Warning: A component is changing a controlled input of type text to be uncontrolled. Input elements should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled input element for the lifetime of the component. ``` I have tried as many ways as possible to just use one or the other (i.e. defaultValue or value) to resolve these warnings but so have not being able to replicate the same functionality that works. * fix: Remove new line * fix: Remove complexity. See detailed commit comments Progress update: * ASSUMPTION: InputNumber may be used in future for useful error, warning, and info messages, so leave the boilerplate props along with Notification. * ASSUMPTION: Displaying an info message in scientific notation i.e. 3e+7 causes more confusion than benefit since most users are non-techy. In future this when we have Issue #86 Settings App, it could be added as an optional extra that could be toggled by the user along with Issue #224 E-Notation * ACTIONS: * [x] - Remove display of scientific notation equivalent * [x] - Remove warning 'number.warn.zero' when user enters 0 value * [x] - Remove info notification 'number.info.scientific' when user enters valid value * [x] - Remove `toScientificNotation` function, which is incorrectly passed the TranslationFunction `t` anyway. * [x] - Remove `IsValidWithMessage` type and replace return value with just a boolean of whether its valid or not * [x] - Move `isValidNumber` into React componet, and do not pass the TranslationFunctio to it! * [x] - Move `isValidKey` into React componet, and do not pass the TranslationFunctio to it! * [x] - Move `isNonDecimal` into React componet, and do not pass the TranslationFunctio to it! * [X] - Remove `isNonInteger` and isNonInteger.ts and isNonInteger.spec.js since no longer using * [X] - Remove `error`, `warn`, `info` from InputNumber state, since we're filtering out everything not valid. Instead add them as optional props (so Balance or another component can display notifications if required) * [X] - Move tests for isValidKey into InputNumber React component, and remove isValidKey.spec.js * [X] - Move tests for isValidNumber into InputNumberReact component, and remove isValidNumber.spec.js * [X] - Move tests for isNonDecimal into InputNumberReact component, and remove isNonDecimal.spec.js * [X] - Update InputNumber.spec.js to remove useless tests that check that the Notification component isn't shown * [X] - Make methods `private` as relevant * [X] - move chainSpec and keyboard subfolder files into InputNumber. Move contents of constants into InputNumber except for BIT_LENGTH_128 * [X] - Move all the different files that are only used in the number input into functions inside that input. Then we also get rid of calling external functions (like isNumberValid) with t as a param. Keep it small, keep it together. * [X] - Remove `cursorIndexInputField` function since we're not trying to check the current cursor position in the input field anymore * [X] - Assume that based on @jacogr's comment: "I'm struggling to see where we are actually now doing errors in the Balance input - it filters everything non-valid.". * Action: Change isValid from being true, to being assigned to the value of !isError * Note: DO NOT update the Extrinsics component to use the 'fees' observable (as it's used in the Transfer App) so the Balance input field is invalid (appears "red" and Submit Transaction button disabled) when the value entered plus fees exceeds the sender's balance. Reasoning: We cannot determine the size of the tx in there at all - there is a FIXME for the hardcoding of the size (already wrong for poc-3), in the Transfer App. So without the final signed size, it currently cannot be done. `fee = baseTxFee + (tx-size-in-bytes * feePerByte)`. We cannot get the tx-size-in-bytes atm. (hardcoded to 156 in Transfer, which assumes only publicKeys and is not the case when we get to poc-3). There is an issue logged that goes through all the fees. Signer needs to have a method exposed that takes all the params as given, encodes the extrinsic and returns the total fee or size. So nothing stopping us, it just has not been done properly. (Signer does not even know in the modal what the final size would be - that is the first stop, showing the fees in there. Then the apps, then maybe extrinsics if it still has a use) * [X] - "Seems SUI docs are currently broken. But in the cases where you absolutely need to point to an input with an attachment, their Label does exactly that and allows attachment with a pointer to an Input. So at most Notification should just warp that. And it really should only be used is completely exceptionally circumstances. (And I really couldn't see any such circumstances from playing with the actual inputs. So at this point seems like a good, although unused in practice idea.)" * [X] - Remove Notification component, and all the error, warn, and info props, as we won't be showing notifications * [ ] - Integrate into the Transfer App `send a value of` where actual transfers will be done (see @jacogr comment and screenshot). Incorporate the Balance/InputNumber into the Transfer App, which currently just uses an <Input> field and isError={!hasAvailable} to determine highlight the input field "red" and disable the Submit button when they don't have sufficient balance. * ACTION: Unable to get this to work. If I change the Input to Balance in Transfer.tsx to the following, then when I modify the value in the Transfer tabs "send a value of" input field, it shows an `Error: Assertion failed` in the console ``` {/* <Input defaultValue='0' isError={!hasAvailable} label={t('amount', { defaultValue: 'send a value of' })} min={0} onChange={this.onChangeAmount} type='number' /> */} <Balance defaultValue={{ value: '0' }} isError={!hasAvailable} label={t('transfer.amount', { defaultValue: 'send a value of' })} onChange={this.onChangeAmount} /> ``` Error mentioned above: ``` InputNumber.tsx?299c:129 Error: Assertion failed at assert (bn.js?cda7:6) at BN._initArray (bn.js?cda7:145) at BN.init [as _init] (bn.js?cda7:82) at new BN (bn.js?cda7:39) at Transfer.onChangeAmount.amount (Transfer.tsx?f63a:45) at Balance.onChange.value (Balance.tsx?8315:42) at InputNumber.onChange.value (InputNumber.tsx?299c:127) at Object.Input.onChange.event [as onChange] (Input.tsx?a939:36) at apply (_apply.js?59ad:15) at baseInvoke (_baseInvoke.js?199b:21) ``` * fix: Move BIT_LENGTH_128 into InputNumber * refactor: Remove disused keys from constant. Remove useless isShift fn. Use Spread operator. * refactor: Simplify comments * refactor: remove useless maxSafeIntegerBN * refactor: remove useless check for isZero * refactor: Simplify isNonDecimal function * refactor: remove maxValue from being maxLength arg. remove useless defaultMaxLength fn * fix: Use InputNumber in Transfer instead of Balance * refactor: move non-number specific keyboard functions to Input instead of InputNumber * refactor: Reorder alphabetically exports * refactor: Remove disused withApi and translate from Balance and simplify defaultValue * refactor: spread operator to pass down props before overrides if any * test: Add more isValidNumber tests. Add maxValue and isValidBitLength tests * fix: fix maxValue function * test: Add maxLength test for 128 bit * refactor: remove unnecessary line in test * fix: Remove support for decimals since BN.js only supports integers * refactor: optimise by using isNaN instead of isNonInteger for single character checks * fix: revert using spread operator to pass down props * fix: Add bitLength prop to InputNumber. Move maxConservativeLength function. See detailed comments * bitLength passed down as prop from Transfer and Balance to InputNumber is 128 bit for latest chain specification * Move maxLength method out of InputNumber class and rename maxConservativeLength also to avoid name conflict with the maxLength prop * Change argument of maxConservativeLength to the maxValueLength to simplify * Update InputNumber tests * Add default bit length for InputNumber is 32 bit for normal numbers * fix: Add BitLength type and BitLengthOption enum * fix: prevent user entering value preceded by 0 * refactor: Replace ternary with more readable * feat: Add 8 and 16 to BitLength for future-proofing and consistency
This commit is contained in:
parent
8b9080566a
commit
0c8e19911c
@ -2,16 +2,17 @@
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the ISC license. See the LICENSE file for details.
|
||||
|
||||
import { I18nProps } from '@polkadot/ui-app/types';
|
||||
import { BitLength, I18nProps } from '@polkadot/ui-app/types';
|
||||
import { RxFees } from '@polkadot/ui-react-rx/ApiObservable/types';
|
||||
import { QueueProps } from '@polkadot/ui-signer/types';
|
||||
import { Fees } from './types';
|
||||
|
||||
import BN from 'bn.js';
|
||||
import React from 'react';
|
||||
import { BitLengthOption } from '@polkadot/ui-app/constants';
|
||||
import AddressSummary from '@polkadot/ui-app/AddressSummary';
|
||||
import InputAddress from '@polkadot/ui-app/InputAddress';
|
||||
import Input from '@polkadot/ui-app/Input';
|
||||
import InputNumber from '@polkadot/ui-app/InputNumber';
|
||||
import withMulti from '@polkadot/ui-react-rx/with/multi';
|
||||
import withObservable from '@polkadot/ui-react-rx/with/observable';
|
||||
import { QueueConsumer } from '@polkadot/ui-signer/Context';
|
||||
@ -31,6 +32,8 @@ type State = {
|
||||
txfees: Fees
|
||||
};
|
||||
|
||||
const DEFAULT_BITLENGTH = BitLengthOption.CHAIN_SPEC as BitLength;
|
||||
|
||||
const ZERO = new BN(0);
|
||||
|
||||
class Transfer extends React.PureComponent<Props, State> {
|
||||
@ -79,15 +82,13 @@ class Transfer extends React.PureComponent<Props, State> {
|
||||
<div className='transfer--Transfer-info'>
|
||||
{this.renderAddress(from)}
|
||||
<div className='transfer--Transfer-data'>
|
||||
<Input
|
||||
defaultValue='0'
|
||||
<InputNumber
|
||||
bitLength={DEFAULT_BITLENGTH}
|
||||
isError={!hasAvailable}
|
||||
label={t('amount', {
|
||||
label={t('transfer.amount', {
|
||||
defaultValue: 'send a value of'
|
||||
})}
|
||||
min={0}
|
||||
onChange={this.onChangeAmount}
|
||||
type='number'
|
||||
/>
|
||||
<FeeDisplay
|
||||
className='medium'
|
||||
@ -130,8 +131,8 @@ class Transfer extends React.PureComponent<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
private onChangeAmount = (amount: string) => {
|
||||
this.setState({ amount: new BN(amount || 0) });
|
||||
private onChangeAmount = (amount: BN) => {
|
||||
this.setState({ amount });
|
||||
}
|
||||
|
||||
private onChangeFrom = (from: Uint8Array) => {
|
||||
|
||||
@ -6,7 +6,6 @@ import { BareProps } from './types';
|
||||
|
||||
import React from 'react';
|
||||
import SUIInput from 'semantic-ui-react/dist/commonjs/elements/Input/Input';
|
||||
|
||||
import isUndefined from '@polkadot/util/is/undefined';
|
||||
|
||||
import Labelled from './Labelled';
|
||||
@ -25,10 +24,12 @@ type Props = BareProps & {
|
||||
isHidden?: boolean,
|
||||
label?: any, // node?
|
||||
max?: any,
|
||||
maxLength?: number,
|
||||
min?: any,
|
||||
name?: string,
|
||||
onChange: (value: string) => void,
|
||||
onKeyDown?: (event: React.KeyboardEvent<Element>) => void,
|
||||
onKeyUp?: (event: React.KeyboardEvent<Element>) => void,
|
||||
placeholder?: string,
|
||||
tabIndex?: number,
|
||||
type?: Input$Type,
|
||||
@ -40,6 +41,39 @@ type State = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
// note: KeyboardEvent.keyCode and KeyboardEvent.which are deprecated
|
||||
const KEYS = {
|
||||
A: 'a',
|
||||
ALT: 'Alt',
|
||||
ARROW_LEFT: 'ArrowLeft',
|
||||
ARROW_RIGHT: 'ArrowRight',
|
||||
BACKSPACE: 'Backspace',
|
||||
C: 'c',
|
||||
CMD: 'Meta',
|
||||
CTRL: 'Control',
|
||||
ENTER: 'Enter',
|
||||
ESCAPE: 'Escape',
|
||||
TAB: 'Tab',
|
||||
V: 'v',
|
||||
X: 'x',
|
||||
ZERO: '0'
|
||||
};
|
||||
|
||||
const KEYS_PRE: Array<any> = [KEYS.ALT, KEYS.CMD, KEYS.CTRL];
|
||||
|
||||
// reference: degrade key to keyCode for cross-browser compatibility https://www.w3schools.com/jsref/event_key_keycode.asp
|
||||
const isCopy = (key: string, isPreKeyDown: boolean): boolean =>
|
||||
isPreKeyDown && key === KEYS.C;
|
||||
|
||||
const isCut = (key: string, isPreKeyDown: boolean): boolean =>
|
||||
isPreKeyDown && key === KEYS.X;
|
||||
|
||||
const isPaste = (key: string, isPreKeyDown: boolean): boolean =>
|
||||
isPreKeyDown && key === KEYS.V;
|
||||
|
||||
const isSelectAll = (key: string, isPreKeyDown: boolean): boolean =>
|
||||
isPreKeyDown && key === KEYS.A;
|
||||
|
||||
let counter = 0;
|
||||
|
||||
export default class Input extends React.PureComponent<Props, State> {
|
||||
@ -48,7 +82,7 @@ export default class Input extends React.PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
render () {
|
||||
const { autoFocus = false, children, className, defaultValue, icon, isEditable = false, isAction = false, isDisabled = false, isError = false, isHidden = false, label, max, min, name, placeholder, style, tabIndex, type = 'text', value, withLabel } = this.props;
|
||||
const { autoFocus = false, children, className, defaultValue, icon, isEditable = false, isAction = false, isDisabled = false, isError = false, isHidden = false, label, max, maxLength, min, name, placeholder, style, tabIndex, type = 'text', value, withLabel } = this.props;
|
||||
|
||||
return (
|
||||
<Labelled
|
||||
@ -73,9 +107,11 @@ export default class Input extends React.PureComponent<Props, State> {
|
||||
hidden={isHidden}
|
||||
max={max}
|
||||
min={min}
|
||||
maxLength={maxLength}
|
||||
name={name || this.state.name}
|
||||
onChange={this.onChange}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onKeyUp={this.onKeyUp}
|
||||
placeholder={placeholder}
|
||||
tabIndex={tabIndex}
|
||||
type={type}
|
||||
@ -98,8 +134,9 @@ export default class Input extends React.PureComponent<Props, State> {
|
||||
|
||||
onChange = (event: React.SyntheticEvent<Element>): void => {
|
||||
const { onChange } = this.props;
|
||||
const { value } = event.target as HTMLInputElement;
|
||||
|
||||
onChange((event.target as HTMLInputElement).value);
|
||||
onChange(value);
|
||||
}
|
||||
|
||||
onKeyDown = (event: React.KeyboardEvent<Element>): void => {
|
||||
@ -109,4 +146,21 @@ export default class Input extends React.PureComponent<Props, State> {
|
||||
onKeyDown(event);
|
||||
}
|
||||
}
|
||||
|
||||
onKeyUp = (event: React.KeyboardEvent<Element>): void => {
|
||||
const { onKeyUp } = this.props;
|
||||
|
||||
if (onKeyUp) {
|
||||
onKeyUp(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
isCopy,
|
||||
isCut,
|
||||
isPaste,
|
||||
isSelectAll,
|
||||
KEYS,
|
||||
KEYS_PRE
|
||||
};
|
||||
|
||||
102
packages/ui-app/src/InputNumber.spec.js
Normal file
102
packages/ui-app/src/InputNumber.spec.js
Normal file
@ -0,0 +1,102 @@
|
||||
// Copyright 2017-2018 @polkadot/ui-app authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the ISC license. See the LICENSE file for details.
|
||||
|
||||
import BN from 'bn.js';
|
||||
import React from 'react';
|
||||
|
||||
import { mount } from '../../../test/enzyme';
|
||||
import { BitLengthOption } from './constants';
|
||||
import Input from './Input';
|
||||
import { InputNumber, maxConservativeLength } from './InputNumber';
|
||||
|
||||
const mockT = (key, options) => (key);
|
||||
|
||||
describe('InputNumber', () => {
|
||||
const DEFAULT_BITLENGTH = BitLengthOption.CHAIN_SPEC;
|
||||
const max128BN = new BN(2).pow(new BN(128)).subn(1);
|
||||
const aboveMax128BN = max128BN.addn(1);
|
||||
const belowMax128BN = max128BN.subn(1);
|
||||
|
||||
let wrapper, divInputNumber, defaultValue;
|
||||
|
||||
beforeEach(() => {
|
||||
defaultValue = { value: '0' };
|
||||
wrapper = mount(
|
||||
<InputNumber
|
||||
defaultValue={defaultValue}
|
||||
t={mockT}
|
||||
/>, {}
|
||||
);
|
||||
divInputNumber = wrapper.find('.ui--InputNumber');
|
||||
});
|
||||
|
||||
it('creates the element', () => {
|
||||
expect(wrapper).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should display InputNumber wrapper and Input component', () => {
|
||||
expect(divInputNumber).toHaveLength(1);
|
||||
expect(wrapper.find(Input)).toHaveLength(1);
|
||||
});
|
||||
|
||||
describe('maxLength', () => {
|
||||
it('returns max length for use on number input fields that is the length of the max BN value minus a conservativeness factor for 128 bit', () => {
|
||||
const maxValueLength = wrapper.instance().maxValue(DEFAULT_BITLENGTH).toString().length;
|
||||
expect(maxConservativeLength(maxValueLength)).toBe(38);
|
||||
});
|
||||
});
|
||||
|
||||
describe('maxValue', () => {
|
||||
it('returns max BN value for given bitlength or uses 128 bit bitlength fallback', () => {
|
||||
expect(wrapper.instance().maxValue(128)).toEqual(max128BN);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isNonInteger', () => {
|
||||
describe('invalid inputs', () => {
|
||||
it('detects decimal value', () => {
|
||||
expect(wrapper.instance().isNonInteger('12.1')).toBe(true);
|
||||
});
|
||||
|
||||
it('detects non-integer value', () => {
|
||||
expect(wrapper.instance().isNonInteger('12.1copypastedvalue')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('valid inputs', () => {
|
||||
it('detects integers', () => {
|
||||
expect(wrapper.instance().isNonInteger('12')).toBe(false);
|
||||
});
|
||||
|
||||
it('detects integer value with single digit', () => {
|
||||
expect(wrapper.instance().isNonInteger('0')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isValidBitLength', () => {
|
||||
it('returns whether value in BN is valid for given bitlength or uses 128 bit bitlength fallback', () => {
|
||||
expect(wrapper.instance().isValidBitLength(aboveMax128BN, DEFAULT_BITLENGTH)).toBe(false);
|
||||
expect(wrapper.instance().isValidBitLength(max128BN, DEFAULT_BITLENGTH)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isValidNumber', () => {
|
||||
describe('checks extrinsic balance', () => {
|
||||
it('throws an error if input value for comparison is not a string', () => {
|
||||
const invalidInputValueType = 340282366920938463463374607431768211456;
|
||||
|
||||
expect(() => { wrapper.instance().isValidNumber(invalidInputValueType, '0', DEFAULT_BITLENGTH); }).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not be valid when user enters positive value greater than or equal to the 128 bit max for latest chain', () => {
|
||||
expect(wrapper.instance().isValidNumber(max128BN.toString(10), DEFAULT_BITLENGTH)).toBe(false);
|
||||
});
|
||||
|
||||
it('should be valid when user enters positive value less than the 128 bit max for latest chain', () => {
|
||||
expect(wrapper.instance().isValidNumber(belowMax128BN.toString(10), DEFAULT_BITLENGTH)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
215
packages/ui-app/src/InputNumber.tsx
Normal file
215
packages/ui-app/src/InputNumber.tsx
Normal file
@ -0,0 +1,215 @@
|
||||
// Copyright 2017-2018 @polkadot/ui-app authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the ISC license. See the LICENSE file for details.
|
||||
|
||||
import { BareProps, BitLength, I18nProps } from './types';
|
||||
|
||||
import BN from 'bn.js';
|
||||
import React from 'react';
|
||||
import isString from '@polkadot/util/is/string';
|
||||
|
||||
import classes from './util/classes';
|
||||
import { BitLengthOption } from './constants';
|
||||
import Input, { KEYS, KEYS_PRE, isCopy, isCut, isPaste, isSelectAll } from './Input';
|
||||
import translate from './translate';
|
||||
|
||||
type Props = BareProps & I18nProps & {
|
||||
bitLength?: BitLength,
|
||||
defaultValue?: string,
|
||||
isError?: boolean,
|
||||
label?: any,
|
||||
maxLength?: number,
|
||||
onChange: (value: BN) => void,
|
||||
placeholder?: string,
|
||||
withLabel?: boolean
|
||||
};
|
||||
|
||||
type State = {
|
||||
isPreKeyDown: boolean,
|
||||
isValid: boolean,
|
||||
previousValue: string
|
||||
};
|
||||
|
||||
const DEFAULT_BITLENGTH = BitLengthOption.NORMAL_NUMBERS as BitLength;
|
||||
const KEYS_ALLOWED: Array<any> = [KEYS.ARROW_LEFT, KEYS.ARROW_RIGHT, KEYS.BACKSPACE, KEYS.ENTER, KEYS.ESCAPE, KEYS.TAB];
|
||||
|
||||
function maxConservativeLength (maxValueLength: number): number {
|
||||
const conservativenessFactor = 1;
|
||||
|
||||
return maxValueLength - conservativenessFactor;
|
||||
}
|
||||
|
||||
class InputNumber extends React.PureComponent<Props, State> {
|
||||
state: State = {
|
||||
isPreKeyDown: false,
|
||||
isValid: false,
|
||||
previousValue: '0'
|
||||
};
|
||||
|
||||
render () {
|
||||
const { bitLength = DEFAULT_BITLENGTH, className, defaultValue, maxLength, style, t } = this.props;
|
||||
const { isValid, previousValue } = this.state;
|
||||
const revertedValue = !isValid ? previousValue : undefined;
|
||||
const maxValueLength = this.maxValue(bitLength).toString().length;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classes('ui--InputNumber', className)}
|
||||
style={style}
|
||||
>
|
||||
<Input
|
||||
{...this.props}
|
||||
defaultValue={defaultValue || '0'}
|
||||
maxLength={maxLength || maxConservativeLength(maxValueLength)}
|
||||
onChange={this.onChange}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onKeyUp={this.onKeyUp}
|
||||
placeholder={t('inputnumber.placeholder', {
|
||||
defaultValue: 'Positive number'
|
||||
})}
|
||||
type='text'
|
||||
value={revertedValue}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private maxValue = (bitLength?: number): BN =>
|
||||
new BN(2).pow(new BN(bitLength || DEFAULT_BITLENGTH)).subn(1)
|
||||
|
||||
// check if string value is non-integer even if above MAX_SAFE_INTEGER. isNaN(Number(value)) is faster for values of length 1
|
||||
private isNonInteger = (value: string): boolean =>
|
||||
value && value.length && value.split('').find(value => '0123456789'.indexOf(value) === -1) ? true : false
|
||||
|
||||
private isValidBitLength = (value: BN, bitLength?: number): boolean =>
|
||||
value.bitLength() <= (bitLength || DEFAULT_BITLENGTH)
|
||||
|
||||
private isValidKey = (event: React.KeyboardEvent<Element>, isPreKeyDown: boolean): boolean => {
|
||||
const { value: previousValue } = event.target as HTMLInputElement;
|
||||
// prevents entry of zero if initial digit is zero
|
||||
const isDuplicateZero = previousValue[0] === '0' && event.key === KEYS.ZERO;
|
||||
|
||||
if (isDuplicateZero) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// prevent use of shift key
|
||||
if (event.shiftKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// allow cut/copy/paste combinations but not non-numeric letters (i.e. a, c, x, v) individually
|
||||
if (
|
||||
(isSelectAll(event.key, isPreKeyDown)) ||
|
||||
(isCut(event.key, isPreKeyDown)) ||
|
||||
(isCopy(event.key, isPreKeyDown)) ||
|
||||
(isPaste(event.key, isPreKeyDown))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isNaN(Number(event.key)) && !KEYS_ALLOWED.includes(event.key)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private isValidNumber = (input: string, bitLength?: number): boolean => {
|
||||
const { t } = this.props;
|
||||
bitLength = bitLength || DEFAULT_BITLENGTH;
|
||||
|
||||
// failsafe as expects only positive integers as permitted by onKeyDown from input of type text
|
||||
if (!isString(input)) {
|
||||
throw Error(t('inputnumber.error.string.required', {
|
||||
defaultValue: 'Number input value must be valid type'
|
||||
}));
|
||||
}
|
||||
|
||||
// remove spaces even though not possible as user restricted from entering spacebar key in onKeyDown
|
||||
input = input.toLowerCase().split(' ').join('');
|
||||
|
||||
if (this.isNonInteger(input)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// max is (2 ** 128 - 1) for bitLength of 128 bit
|
||||
const maxBN = this.maxValue(bitLength);
|
||||
const inputBN = new BN(input);
|
||||
|
||||
if (!inputBN.lt(maxBN) || !this.isValidBitLength(inputBN, bitLength)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private onChange = (value: string): void => {
|
||||
const { bitLength, onChange } = this.props;
|
||||
|
||||
try {
|
||||
const valueBN = new BN(value || 0);
|
||||
const isValid = this.isValidNumber(value, bitLength);
|
||||
|
||||
this.setState({ isValid });
|
||||
|
||||
if (!onChange || !isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
onChange(valueBN);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
private onKeyDown = (event: React.KeyboardEvent<Element>): void => {
|
||||
const { isPreKeyDown } = this.state;
|
||||
const { value: previousValue } = event.target as HTMLInputElement;
|
||||
|
||||
// store previous input field in state incase user pastes invalid value and we need to revert the input value
|
||||
this.setState({ previousValue });
|
||||
|
||||
if (KEYS_PRE.includes(event.key)) {
|
||||
this.setState({ isPreKeyDown: true });
|
||||
}
|
||||
|
||||
// restrict input of certain keys
|
||||
const isValid = this.isValidKey(event, isPreKeyDown);
|
||||
|
||||
if (!isValid) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
private onKeyUp = (event: React.KeyboardEvent<Element>): void => {
|
||||
const { value: newValue } = event.target as HTMLInputElement;
|
||||
const isNewValueZero = new BN(newValue).isZero();
|
||||
|
||||
if (KEYS_PRE.includes(event.key)) {
|
||||
this.setState({ isPreKeyDown: false });
|
||||
}
|
||||
|
||||
/* if new value equates to '0' in BN when but it's length is >=1 (i.e. '012', '00', etc)
|
||||
* then replace the input value with just '0'.
|
||||
* otherwise remove the preceding zeros from the new value (i.e. '0123' -> '123')
|
||||
* note: edge case glitch occurs if existing value is '0' and you 'hold down' and keep
|
||||
* pasting a value of '00' after it, then sometimes when you let go the
|
||||
* remaining value shown as '000' or '00000' in the UI, but it's still ok because
|
||||
* the actual BN if the user submitted would still be '0', and if they then press any key
|
||||
* the UI input value resets to '0'
|
||||
*/
|
||||
if (isNewValueZero && newValue.length >= 1) {
|
||||
(event.target as HTMLInputElement).value = '0';
|
||||
} else {
|
||||
(event.target as HTMLInputElement).value = newValue.replace(/^0+/, '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
InputNumber,
|
||||
maxConservativeLength
|
||||
};
|
||||
|
||||
export default translate(InputNumber);
|
||||
33
packages/ui-app/src/Params/Param/Balance.spec.js
Normal file
33
packages/ui-app/src/Params/Param/Balance.spec.js
Normal file
@ -0,0 +1,33 @@
|
||||
// Copyright 2017-2018 @polkadot/ui-app authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the ISC license. See the LICENSE file for details.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { shallow } from '../../../../../test/enzyme';
|
||||
import InputNumber from '../../InputNumber';
|
||||
import { Balance } from './Balance';
|
||||
|
||||
const mockT = (key, options) => (key);
|
||||
|
||||
describe('Balance', () => {
|
||||
let wrapper, defaultValue;
|
||||
|
||||
beforeEach(() => {
|
||||
defaultValue = { value: '0' };
|
||||
wrapper = shallow(
|
||||
<Balance
|
||||
defaultValue={defaultValue}
|
||||
t={mockT}
|
||||
/>, {}
|
||||
);
|
||||
});
|
||||
|
||||
it('creates the element', () => {
|
||||
expect(wrapper).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should display InputNumber component', () => {
|
||||
expect(wrapper.find(InputNumber)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
60
packages/ui-app/src/Params/Param/Balance.tsx
Normal file
60
packages/ui-app/src/Params/Param/Balance.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
// Copyright 2017-2018 @polkadot/ui-app authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the ISC license. See the LICENSE file for details.
|
||||
|
||||
import { BitLength } from '../../types';
|
||||
import { Props as BareProps } from '../types';
|
||||
|
||||
import BN from 'bn.js';
|
||||
import React from 'react';
|
||||
|
||||
import { BitLengthOption } from '../../constants';
|
||||
import InputNumber from '../../InputNumber';
|
||||
import Bare from './Bare';
|
||||
|
||||
type Props = BareProps;
|
||||
|
||||
const DEFAULT_BITLENGTH = BitLengthOption.CHAIN_SPEC as BitLength;
|
||||
|
||||
class Balance extends React.PureComponent<Props> {
|
||||
render () {
|
||||
const { className, defaultValue: { value }, isError, label, style, withLabel } = this.props;
|
||||
const defaultValue = new BN(value as BN || '0').toString(10);
|
||||
|
||||
return (
|
||||
<Bare
|
||||
className={className}
|
||||
style={style}
|
||||
>
|
||||
<InputNumber
|
||||
bitLength={DEFAULT_BITLENGTH}
|
||||
className='large'
|
||||
defaultValue={defaultValue || '0'}
|
||||
isError={isError}
|
||||
label={label}
|
||||
onChange={this.onChange}
|
||||
withLabel={withLabel}
|
||||
/>
|
||||
</Bare>
|
||||
);
|
||||
}
|
||||
|
||||
onChange = (value: BN): void => {
|
||||
const { isError, onChange } = this.props;
|
||||
|
||||
if (!onChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
onChange({
|
||||
isValid: !isError,
|
||||
value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
Balance
|
||||
};
|
||||
|
||||
export default Balance;
|
||||
@ -7,6 +7,7 @@ import { Props, ComponentMap } from '../types';
|
||||
|
||||
import Account from './Account';
|
||||
import Amount from './Amount';
|
||||
import Balance from './Balance';
|
||||
import Bool from './Bool';
|
||||
import Bytes from './Bytes';
|
||||
import Code from './Code';
|
||||
@ -22,7 +23,7 @@ import VoteThreshold from './VoteThreshold';
|
||||
const components: ComponentMap = {
|
||||
'AccountId': Account,
|
||||
'AccountIndex': Amount,
|
||||
'Balance': Amount,
|
||||
'Balance': Balance,
|
||||
'BlockNumber': Amount,
|
||||
'bool': Bool,
|
||||
'Bytes': Bytes,
|
||||
|
||||
8
packages/ui-app/src/constants.ts
Normal file
8
packages/ui-app/src/constants.ts
Normal file
@ -0,0 +1,8 @@
|
||||
// Copyright 2017-2018 @polkadot/ui-app authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the ISC license. See the LICENSE file for details.
|
||||
|
||||
export enum BitLengthOption {
|
||||
CHAIN_SPEC = 128,
|
||||
NORMAL_NUMBERS = 32
|
||||
}
|
||||
2
packages/ui-app/src/types.d.ts
vendored
2
packages/ui-app/src/types.d.ts
vendored
@ -25,3 +25,5 @@ export type BaseContext = {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export type BitLength = 8 | 16 | 32 | 64 | 128 | 256;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user