diff --git a/package.json b/package.json index dfb395e..29a8728 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dotenv": "^5.0.1", "elasticsearch": "^14.2.1", "file-saver": "^1.3.8", + "final-form": "^4.5.2", "html5-websocket": "^2.0.2", "http-aws-es": "^4.0.0", "lodash": "^4.17.5", @@ -22,6 +23,7 @@ "react-copy-to-clipboard": "^5.0.1", "react-dom": "^16.2.0", "react-file-reader-input": "^1.1.4", + "react-final-form": "^3.3.1", "react-router-dom": "^4.2.2", "react-scripts-ts": "2.14.0", "recompose": "^0.26.0", diff --git a/src/form/formWrapper.ts b/src/form/formWrapper.ts new file mode 100644 index 0000000..1e5b847 --- /dev/null +++ b/src/form/formWrapper.ts @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2018 Matus Zamborsky + * This file is part of The ONT Detective. + * + * The ONT Detective is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The ONT Detective is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with The ONT Detective. If not, see . + */ + + import { compose, mapProps } from 'recompose'; + import { get } from 'lodash'; + import { Form as SemanticForm, FormFieldProps, FormProps } from 'semantic-ui-react'; + import { FieldRenderProps, FormRenderProps } from 'react-final-form'; + + export const FieldWrapper = compose( + mapProps((outer) => ({ + ...outer, + meta: undefined, + input: { + ...outer.input, + value: outer.input.value + }, + error: outer.meta.touched && outer.meta.invalid, + onChange: outer.input.onChange + })) + ); + + export const InputField = FieldWrapper(SemanticForm.Input); + export const TextareaField = FieldWrapper(SemanticForm.TextArea); + + export const FormWrapper = compose( + mapProps((outer) => ({ + onSubmit: outer.handleSubmit, + children: get(outer, 'children') + })) + ); + + export const Form = FormWrapper(SemanticForm); diff --git a/src/home/home.ts b/src/home/home.ts index 0e7b4cc..b584135 100644 --- a/src/home/home.ts +++ b/src/home/home.ts @@ -17,7 +17,7 @@ */ import { compose, withHandlers, withState, flattenProp } from 'recompose'; -import { InputOnChangeData } from 'semantic-ui-react'; +import { get } from 'lodash'; import { getBlockByIndex, getBlock } from '~/shared/blocksApi'; import { getTransaction } from '~/shared/transactionsApi'; import { getAccount } from '~/shared/accountsApi'; @@ -38,13 +38,10 @@ function isBlockIndex(q: string): boolean { interface State { redirect?: string; - invalid?: boolean; - q: string; } interface Handlers { - handleSearch: () => void; - handleSearchChange: (e: React.SyntheticEvent, data: InputOnChangeData) => void; + handleSearch: (values: object) => void; } export interface PropsInner extends State, Handlers { @@ -52,18 +49,17 @@ export interface PropsInner extends State, Handlers { export default compose( withState('state', 'setState', { - q: '' }), withHandlers, Handlers>({ - handleSearch: ({state, setState}) => async () => { - let q = state.q.trim(); + handleSearch: ({state, setState}) => async (values) => { + let q = get(values, 'q').trim(); if (isAddress(q)) { try { await getAccount(q); setState({...state, redirect: `/accounts/${q}`}); } catch (e) { - setState({...state, invalid: true}); + return Promise.resolve({q: 'Invalid address.'}); } } else if (isBlockOrTransaction(q)) { if (q.length === 66) { @@ -77,7 +73,7 @@ export default compose( await getTransaction(q); setState({...state, redirect: `/transactions/${q}`}); } catch (e) { - setState({...state, invalid: true}); + return Promise.resolve({q: 'Invalid block or transaction hash.'}); } } } else if (isBlockIndex(q)) { @@ -85,17 +81,14 @@ export default compose( const block = await getBlockByIndex(Number(q)); setState({...state, redirect: `/blocks/${block.Hash}`}); } catch (e) { - setState({...state, invalid: true}); + return Promise.resolve({q: 'Invalid block index.'}); } } else { - setState({...state, invalid: true}); + return Promise.resolve({q: 'Invalid search term.'}); } + + return Promise.resolve({}); }, - handleSearchChange: ({state, setState}) => ( - e: React.SyntheticEvent, data: InputOnChangeData - ) => { - setState({...state, invalid: false, q: data.value !== undefined ? data.value : ''}); - } }), flattenProp('state') )(View); diff --git a/src/home/homeView.tsx b/src/home/homeView.tsx index bc8bf3d..21eb8f5 100644 --- a/src/home/homeView.tsx +++ b/src/home/homeView.tsx @@ -17,10 +17,12 @@ */ import * as React from 'react'; -import { Grid, Header, Form, Message } from 'semantic-ui-react'; import { Redirect } from 'react-router-dom'; -import './home.css'; +import { Grid, Header, Button, Message } from 'semantic-ui-react'; +import { Form as FinalForm, Field } from 'react-final-form'; +import { InputField, Form } from '~/form/formWrapper'; import { PropsInner as Props } from './home'; +import './home.css'; const logo = require('./detective.svg'); @@ -30,19 +32,20 @@ const Home: React.SFC = (props: Props) => (
ONT Detective
-
- + + Invalid account/block/transaction. - Search - + + {props.redirect != null ? () : null}
diff --git a/src/ontIds/createClaim.ts b/src/ontIds/createClaim.ts index b8e22a8..289d210 100644 --- a/src/ontIds/createClaim.ts +++ b/src/ontIds/createClaim.ts @@ -17,11 +17,11 @@ */ import { compose, withState, withHandlers, flattenProp, withProps } from 'recompose'; +import { get } from 'lodash'; import { RouterProps, match } from 'react-router'; import { withRouter } from 'react-router-dom'; -import { InputOnChangeData, TextAreaProps } from 'semantic-ui-react'; import { StateSetter } from '~/utils'; -import { registerSelfClaim, Errors } from '~/shared/walletApi'; +import { registerSelfClaim } from '~/shared/walletApi'; import View from './createClaimView'; interface PropsOuter { @@ -33,18 +33,13 @@ interface PropsOwn { } interface State { - passwordInput: string; - contextInput: string; - contentInput: string; registering: boolean; - wrong: string | null; } interface Handlers { - handleCreate: () => void; - handlePasswordChange: (e: React.SyntheticEvent, data: InputOnChangeData) => void; - handleContextChange: (e: React.SyntheticEvent, data: InputOnChangeData) => void; - handleContentChange: (e: React.SyntheticEvent, data: TextAreaProps) => void; + handleCreate: (values: object) => void; + handleValidateNotEmpty: (value: string) => boolean; + handleValidateJSon: (value: string) => boolean; } export interface PropsInner extends Handlers, State, PropsOwn, PropsOuter { @@ -56,57 +51,20 @@ export default compose( id: props.match.params.id })), withState, 'state', 'setState'>('state', 'setState', { - passwordInput: '', - contextInput: '', - contentInput: '', - registering: false, - wrong: null + registering: false }), withHandlers & RouterProps, Handlers>({ - handleCreate: (props) => async () => { + handleCreate: (props) => async (values) => { props.setState({ ...props.state, - registering: true, - wrong: null + registering: true }); - const password = props.state.passwordInput; - const context = props.state.contextInput; - const content = props.state.contentInput; - - if (password.trim().length === 0) { - props.setState({ - ...props.state, - registering: false, - wrong: 'Empty password.' - }); - - return; - } - - if (context.trim().length === 0) { - props.setState({ - ...props.state, - registering: false, - wrong: 'Invalid context.' - }); - - return; - } - - let contentJson; - try { - contentJson = JSON.parse(content); - } catch (e) { - props.setState({ - ...props.state, - registering: false, - wrong: 'Invalid JSON content.' - }); - - return; - } - + const password = get(values, 'password'); + const context = get(values, 'context'); + const content = get(values, 'content'); + const contentJson = JSON.parse(content); + try { await registerSelfClaim(props.id, password, context, contentJson); @@ -116,30 +74,24 @@ export default compose( }); props.history.push(`/ont-ids/${props.id}`); + return Promise.resolve({}); } catch (e) { - if (e === Errors.WRONG_PASSWORD) { - props.setState({ - ...props.state, - registering: false, - wrong: 'Invalid password.' - }); - } + props.setState({ + ...props.state, + registering: false, + }); + + return Promise.resolve({ password: 'Invalid password.'}); } }, - handlePasswordChange: ({state, setState}) => ( - e: React.SyntheticEvent, data: InputOnChangeData - ) => { - setState({...state, passwordInput: data.value !== undefined ? data.value : ''}); - }, - handleContextChange: ({state, setState}) => ( - e: React.SyntheticEvent, data: InputOnChangeData - ) => { - setState({...state, contextInput: data.value !== undefined ? data.value : ''}); - }, - handleContentChange: ({state, setState}) => ( - e: React.SyntheticEvent, data: TextAreaProps - ) => { - setState({...state, contentInput: data.value !== undefined ? data.value.toString() : ''}); + handleValidateNotEmpty: (props) => (value) => (value === undefined || value.trim().length === 0), + handleValidateJSon: (props) => (value) => { + try { + JSON.parse(value); + return false; + } catch (e) { + return true; + } } }), flattenProp('state'), diff --git a/src/ontIds/createClaimView.tsx b/src/ontIds/createClaimView.tsx index 1555330..31b617b 100644 --- a/src/ontIds/createClaimView.tsx +++ b/src/ontIds/createClaimView.tsx @@ -18,7 +18,9 @@ import * as React from 'react'; import { Link } from 'react-router-dom'; -import { Breadcrumb, Segment, Header, Loader, Form, Message } from 'semantic-ui-react'; +import { Form as FinalForm, Field } from 'react-final-form'; +import { Breadcrumb, Segment, Header, Loader, Message, Button } from 'semantic-ui-react'; +import { InputField, TextareaField, Form } from '~/form/formWrapper'; import { PropsInner as Props } from './createClaim'; const OntIdView: React.SFC = (props) => ( @@ -46,26 +48,31 @@ const OntIdView: React.SFC = (props) => ( {props.registering ? ( Asserting Claim on blockchain ... ) : ( -
- {props.wrong} - + - - - Create - + + )} diff --git a/src/shared/ontIdApi.ts b/src/shared/ontIdApi.ts index 445cbb9..a775fa1 100644 --- a/src/shared/ontIdApi.ts +++ b/src/shared/ontIdApi.ts @@ -53,8 +53,7 @@ export async function getOntIdsByIds( direction: Direction ): Promise> { const client = getClient(); - console.log('ids', ids); - + const params: SearchParams = { index: Indices.OntId, from, diff --git a/src/shared/walletApi.ts b/src/shared/walletApi.ts index b3ee204..38c1f78 100644 --- a/src/shared/walletApi.ts +++ b/src/shared/walletApi.ts @@ -10,8 +10,8 @@ export enum Errors { WRONG_PASSWORD = 'Wrong password' } -export function registerIdentity(ontId: string, privKey: string): Promise { - return new Promise((resolve, reject) => { +export async function registerIdentity(ontId: string, privKey: string): Promise { + return new Promise((resolve, reject) => { const tx = OntidContract.buildRegisterOntidTx(ontId, privKey); const raw = builder.sendRawTransaction(tx.serialize()); diff --git a/src/wallet/createWallet.ts b/src/wallet/createWallet.ts index a6c5e42..4e23ef3 100644 --- a/src/wallet/createWallet.ts +++ b/src/wallet/createWallet.ts @@ -17,9 +17,9 @@ */ import { compose, withState, withHandlers, flattenProp } from 'recompose'; +import { get } from 'lodash'; import { RouterProps } from 'react-router'; import { withRouter } from 'react-router-dom'; -import { InputOnChangeData } from 'semantic-ui-react'; import { Wallet, scrypt } from 'ont-sdk-ts'; import { StateSetter } from '~/utils'; import { registerIdentity, saveWallet } from '~/shared/walletApi'; @@ -29,18 +29,16 @@ interface PropsOuter { } interface PropsOwn { + initialValues: object; } interface State { - nameInput: string; - passwordInput: string; registering: boolean; } interface Handlers { - handleCreate: () => void; - handleNameChange: (e: React.SyntheticEvent, data: InputOnChangeData) => void; - handlePasswordChange: (e: React.SyntheticEvent, data: InputOnChangeData) => void; + handleCreate: (values: object) => void; + handleValidateNotEmpty: (value: string) => boolean; } export interface PropsInner extends Handlers, State, PropsOwn, PropsOuter { @@ -49,19 +47,17 @@ export interface PropsInner extends Handlers, State, PropsOwn, PropsOuter { export default compose( withRouter, withState, 'state', 'setState'>('state', 'setState', { - nameInput: '', - passwordInput: '', registering: false }), withHandlers & RouterProps, Handlers>({ - handleCreate: (props) => async () => { + handleCreate: (props) => async (values) => { props.setState({ ...props.state, registering: true }); - const name = props.state.nameInput; - const password = props.state.passwordInput; + const name = get(values, 'name', ''); + const password = get(values, 'password', ''); const wallet = Wallet.createIdentityWallet(password, name); @@ -80,16 +76,7 @@ export default compose( props.history.push('/wallet'); }, - handleNameChange: ({state, setState}) => ( - e: React.SyntheticEvent, data: InputOnChangeData - ) => { - setState({...state, nameInput: data.value !== undefined ? data.value : ''}); - }, - handlePasswordChange: ({state, setState}) => ( - e: React.SyntheticEvent, data: InputOnChangeData - ) => { - setState({...state, passwordInput: data.value !== undefined ? data.value : ''}); - } + handleValidateNotEmpty: (props) => (value) => (value === undefined || value.trim().length === 0) }), flattenProp('state'), ) (View); diff --git a/src/wallet/createWalletView.tsx b/src/wallet/createWalletView.tsx index 1547311..2e6783b 100644 --- a/src/wallet/createWalletView.tsx +++ b/src/wallet/createWalletView.tsx @@ -18,7 +18,9 @@ import * as React from 'react'; import { Link } from 'react-router-dom'; -import { Breadcrumb, Segment, Header, Form, Message, Loader } from 'semantic-ui-react'; +import { Form as FinalForm, Field } from 'react-final-form'; +import { Breadcrumb, Segment, Header, Message, Loader, Button } from 'semantic-ui-react'; +import { InputField, Form } from '~/form/formWrapper'; import { PropsInner as Props } from './createWallet'; const CreateWallet: React.SFC = (props) => ( @@ -42,20 +44,24 @@ const CreateWallet: React.SFC = (props) => ( {props.registering ? ( Registering ONT ID on blockchain ... ) : ( -
- + - - Create - + + )} diff --git a/yarn.lock b/yarn.lock index 667f7f0..2bd1b2a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3124,6 +3124,10 @@ fill-range@^4.0.0: repeat-string "^1.6.1" to-regex-range "^2.1.0" +final-form@^4.5.2: + version "4.5.2" + resolved "https://registry.yarnpkg.com/final-form/-/final-form-4.5.2.tgz#f8a9518784bdc2f72e71548a42b04c640963ca49" + finalhandler@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.0.tgz#ce0b6855b45853e791b2fcc680046d88253dd7f5" @@ -6401,6 +6405,10 @@ react-file-reader-input@^1.1.4: dependencies: karma "^0.13.22" +react-final-form@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/react-final-form/-/react-final-form-3.3.1.tgz#dbe40eec853a9c451e5a8deb3b02d948a2fb7e3c" + react-router-dom@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.2.2.tgz#c8a81df3adc58bba8a76782e946cbd4eae649b8d"