diff --git a/.vscode/settings.json b/.vscode/settings.json index 30fbe00..95c0d10 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -31,7 +31,8 @@ "singleline", "textbox", "tsdoc", + "typeahead", "typesafe", "unstyled" ], -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index 64444cc..86ecd9e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1765,6 +1765,15 @@ "csstype": "^3.0.2" } }, + "@types/react-bootstrap-typeahead": { + "version": "3.4.6", + "resolved": "https://registry.npmjs.org/@types/react-bootstrap-typeahead/-/react-bootstrap-typeahead-3.4.6.tgz", + "integrity": "sha512-3vrWrPNb2G+ieyWB1VtEPZH6E4NBcd2cVsacb6oUhBNU/D/ZYnUqnbmeTIKzIVXqEkdPtgARG45W1x7tUV9DGA==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/react-dom": { "version": "16.9.8", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.8.tgz", @@ -3832,6 +3841,11 @@ } } }, + "compute-scroll-into-view": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.16.tgz", + "integrity": "sha512-a85LHKY81oQnikatZYA90pufpZ6sQx++BoCxOEMsjpZx+ZnaKGQnCyCehTRr/1p9GBIAHTjcU9k71kSYWloLiQ==" + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -4041,6 +4055,15 @@ "sha.js": "^2.4.8" } }, + "create-react-context": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/create-react-context/-/create-react-context-0.3.0.tgz", + "integrity": "sha512-dNldIoSuNSvlTJ7slIKC/ZFGKexBMBrrcc+TTe1NdmROnaASuLPvqpwj9v4XS4uXZ8+YPu0sNmShX2rXI5LNsw==", + "requires": { + "gud": "^1.0.0", + "warning": "^4.0.3" + } + }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -6492,6 +6515,11 @@ "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=" }, + "gud": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz", + "integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==" + }, "gzip-size": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz", @@ -8348,6 +8376,11 @@ "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=" }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" + }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -9924,6 +9957,11 @@ "ts-pnp": "^1.1.6" } }, + "popper.js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", + "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==" + }, "portfinder": { "version": "1.0.28", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz", @@ -11203,6 +11241,40 @@ "warning": "^4.0.3" } }, + "react-bootstrap-typeahead": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/react-bootstrap-typeahead/-/react-bootstrap-typeahead-5.1.1.tgz", + "integrity": "sha512-fjbeF9xenZ7AqH+yQr9ad7PvntA4SSmHMDD56F25B/EmN0NAJsqobXMW8/Y2jllMpcziGG2H4A7GyAdQfyVRFA==", + "requires": { + "@babel/runtime": "^7.3.4", + "@restart/hooks": "^0.3.22", + "classnames": "^2.2.0", + "fast-deep-equal": "^3.1.1", + "invariant": "^2.2.1", + "lodash.debounce": "^4.0.8", + "prop-types": "^15.5.8", + "react-overlays": "^2.0.0", + "react-popper": "^1.0.0", + "scroll-into-view-if-needed": "^2.2.20", + "warning": "^4.0.1" + }, + "dependencies": { + "react-overlays": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-2.1.1.tgz", + "integrity": "sha512-gaQJwmb8Ij2IGVt4D1HmLtl4A0mDVYxlsv/8i0dHWK7Mw0kNat6ORelbbEWzaXTK1TqMeQtJw/jraL3WOADz3w==", + "requires": { + "@babel/runtime": "^7.4.5", + "@restart/hooks": "^0.3.12", + "dom-helpers": "^5.1.0", + "popper.js": "^1.15.0", + "prop-types": "^15.7.2", + "uncontrollable": "^7.0.0", + "warning": "^4.0.3" + } + } + } + }, "react-dev-utils": { "version": "10.2.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-10.2.1.tgz", @@ -11459,6 +11531,20 @@ "warning": "^4.0.3" } }, + "react-popper": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.7.tgz", + "integrity": "sha512-nmqYTx7QVjCm3WUZLeuOomna138R1luC4EqkW3hxJUrAe+3eNz3oFCLYdnPwILfn0mX1Ew2c3wctrjlUMYYUww==", + "requires": { + "@babel/runtime": "^7.1.2", + "create-react-context": "^0.3.0", + "deep-equal": "^1.1.1", + "popper.js": "^1.14.4", + "prop-types": "^15.6.1", + "typed-styles": "^0.0.7", + "warning": "^4.0.2" + } + }, "react-redux": { "version": "7.2.1", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.1.tgz", @@ -12304,6 +12390,14 @@ "ajv-keywords": "^3.4.1" } }, + "scroll-into-view-if-needed": { + "version": "2.2.26", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.26.tgz", + "integrity": "sha512-SQ6AOKfABaSchokAmmaxVnL9IArxEnLEX9j4wAZw+x4iUTb40q7irtHG3z4GtAWz5veVZcCnubXDBRyLVQaohw==", + "requires": { + "compute-scroll-into-view": "^1.0.16" + } + }, "scss-tokenizer": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", @@ -13706,6 +13800,11 @@ "mime-types": "~2.1.24" } }, + "typed-styles": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/typed-styles/-/typed-styles-0.0.7.tgz", + "integrity": "sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q==" + }, "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", diff --git a/package.json b/package.json index 1eeee27..dad5120 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "i18next-http-backend": "^1.0.20", "react": "^16.13.1", "react-bootstrap": "^1.3.0", + "react-bootstrap-typeahead": "^5.1.1", "react-dom": "^16.13.1", "react-i18next": "^11.7.2", "react-redux": "^7.2.1", @@ -67,6 +68,7 @@ "@types/node": "^12.12.55", "@types/prop-types": "^15.7.3", "@types/react": "^16.9.49", + "@types/react-bootstrap-typeahead": "^3.4.6", "@types/react-dom": "^16.9.8", "@types/react-redux": "^7.1.9", "@types/react-router-bootstrap": "^0.24.5", diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index f56ae50..a56e707 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -94,7 +94,11 @@ "addWallet": { "dialogTitle": "Add wallet", - "dialogTitleCreate": "Create wallet" + "dialogTitleCreate": "Create wallet", + "walletLabel": "Wallet label", + "walletLabelPlaceholder": "Wallet label (optional)", + "walletCategory": "Wallet category", + "walletCategoryDropdownNone": "No category" }, "credits": { diff --git a/src/index.scss b/src/index.scss index be2feef..ef9333b 100644 --- a/src/index.scss +++ b/src/index.scss @@ -2,4 +2,5 @@ @import "~scss/font"; @import "~scss/variables"; @import "~bootstrap/scss/bootstrap"; +@import "~react-bootstrap-typeahead/css/Typeahead"; @import "~scss/modal"; diff --git a/src/layouts/dialogs/AddWalletDialog.tsx b/src/layouts/dialogs/AddWalletDialog.tsx index aec3480..348d588 100644 --- a/src/layouts/dialogs/AddWalletDialog.tsx +++ b/src/layouts/dialogs/AddWalletDialog.tsx @@ -1,11 +1,18 @@ -import React from "react"; +import React, { Component } from "react"; -import { useTranslation } from "react-i18next"; +import { withTranslation, WithTranslation } from "react-i18next"; import { IconButton } from "@components/icon-button/IconButton"; +import Form from "react-bootstrap/Form"; +import { FormikHelpers } from "formik"; +import { Highlighter, Typeahead } from "react-bootstrap-typeahead"; import { ModalDialog, withDialogLink } from "./ModalDialog"; +import { WalletFormatName } from "@krist/wallets/formats/WalletFormat"; + +import { noop } from "@utils"; + export const AddWalletButton = withDialogLink( (show, handleClose) => @@ -16,7 +23,7 @@ )(IconButton); -interface Props { +interface OwnProps { show: boolean; handleClose: () => void; @@ -24,15 +31,75 @@ * dialog. */ create?: boolean; } +type Props = OwnProps & WithTranslation; -export const AddWalletDialog: React.FC = ({ show, handleClose, create }: Props) => { - const { t } = useTranslation(); +interface FormValues { + label?: string; + category: string; + password: string; + format: WalletFormatName; +} - return - Placeholder - ; +class AddWalletDialogComponent extends Component { + async onSubmit(values: FormValues, helpers: FormikHelpers): Promise { + } + + render() { + const { show, handleClose, create, t } = this.props; + + return + show={show} + handleClose={handleClose} + hasCloseButton={true} + hasFooterCloseButton={true} + title={t(create ? "addWallet.dialogTitleCreate" : "addWallet.dialogTitle")} + initialValues={{ + label: "", + category: "", + password: "", + format: "kristwallet" + }} + onSubmit={this.onSubmit.bind(this)} + > + {({ handleChange, values, errors, setFieldValue }) => <> + {/* Wallet label */} + + {t("addWallet.walletLabel")} + + + {errors.label} + + {/* Category dropdown */} + + {t("addWallet.walletCategory")} + setFieldValue("category", selected[0] || "")} + onInputChange={text => setFieldValue("category", text)} + isInvalid={!!errors.category} + renderMenuItemChildren={(option, { text }) => + option === "" // Show the "No category" text + ? {t("addWallet.walletCategoryDropdownNone")} + : {option} + } + /> + + {errors.category} + } + ; + } }; + +export const AddWalletDialog = withTranslation()(AddWalletDialogComponent); diff --git a/src/layouts/dialogs/ModalDialog.tsx b/src/layouts/dialogs/ModalDialog.tsx index f7704f0..6e7e1da 100644 --- a/src/layouts/dialogs/ModalDialog.tsx +++ b/src/layouts/dialogs/ModalDialog.tsx @@ -34,7 +34,10 @@ } = this.props; if ((hasCloseButton || hasFooterCloseButton) && !handleClose) - throw new Error("FormDialog has close button but no close handler"); + throw new Error("ModalDialog has close button but no close handler"); + + if (initialValues && !onSubmit) + throw new Error("ModalDialog has initialValues but no onSubmit"); // The contents of the modal (header, body, footer), which may or may not // be wrapped in a form. diff --git a/src/shared-components/list-view/ListMobile.tsx b/src/shared-components/list-view/ListMobile.tsx index 23329ab..18ec4be 100644 --- a/src/shared-components/list-view/ListMobile.tsx +++ b/src/shared-components/list-view/ListMobile.tsx @@ -1,6 +1,6 @@ import React, { Component, ReactNode } from "react"; -import { ListGroup } from "react-bootstrap"; +import ListGroup from "react-bootstrap/ListGroup"; import { QueryStateBase, DataStateBase } from "./DataProvider";