From b1ab523def46c4f6a8e2b747def6bf54f0efc682 Mon Sep 17 00:00:00 2001
From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com>
Date: Thu, 10 Sep 2020 11:15:38 -0400
Subject: [PATCH 1/4] feat(ApiUserForm): Add form validation.
---
components/ApiUserForm.js | 278 ++++++++++++++++++++++------------
components/LayoutWithAuth0.js | 16 +-
package.json | 4 +-
util/middleware.js | 3 +-
yarn.lock | 76 +++++++++-
5 files changed, 272 insertions(+), 105 deletions(-)
diff --git a/components/ApiUserForm.js b/components/ApiUserForm.js
index 10d5c0e..e454875 100644
--- a/components/ApiUserForm.js
+++ b/components/ApiUserForm.js
@@ -1,12 +1,25 @@
+import clone from 'clone'
+import { Formik } from 'formik'
import { Component } from 'react'
import { Button, Card, Col, Container, Form, Row } from 'react-bootstrap'
import { withAuth } from 'use-auth0-hooks'
+import * as yup from 'yup'
import { AUTH0_SCOPE } from '../util/constants'
+// The validation schema for the form.
+const validationSchema = yup.object({
+ appName: yup.string().required('Please enter your application name.'),
+ appPurpose: yup.string(),
+ appUrl: yup.string().url('Please enter a valid URL (should start with http:// or https://), or leave blank if unknown.'),
+ company: yup.string().required('Please enter your company name.'),
+ hasConsentedToTerms: yup.boolean().oneOf([true], 'You must agree to the terms to continue.'),
+ name: yup.string().required('Please enter your name.')
+})
+
/**
- * The basic form for creating an ApiUser. This can also be used to show a
- * disabled view of the form (for viewing user details).
+ * The basic form for creating an ApiUser, including input validation.
+ * This can also be used to show a disabled view of the form (for viewing user details).
*
* TODO: Add the ability to update a user?
*/
@@ -15,12 +28,12 @@ class ApiUserForm extends Component {
super(props)
this.state = {
apiUser: {
- appName: null,
- appPurpose: null,
- appUrl: null,
- company: null,
+ appName: '',
+ appPurpose: '',
+ appUrl: '',
+ company: '',
hasConsentedToTerms: false,
- name: null
+ name: ''
}
}
}
@@ -35,13 +48,15 @@ class ApiUserForm extends Component {
this.updateUserState({ hasConsentedToTerms: e.target.checked })
}
- handleCreateAccount = async e => {
+ handleCreateAccount = async apiUserData => {
const { auth, createUser } = this.props
if (auth.user) {
- const { apiUser } = this.state
+ const apiUser = clone(apiUserData)
+
// Add required attributes for middleware storage.
apiUser.auth0UserId = auth.user.sub
apiUser.email = auth.user.email
+
createUser(apiUser)
} else {
alert('Could not save your data (Auth0 id was not available).')
@@ -58,6 +73,10 @@ class ApiUserForm extends Component {
})
}
+ dummy () {
+
+ }
+
render () {
const { createUser } = this.props
// Default values to apiUser passed from props. Otherwise, use original state.
@@ -72,99 +91,162 @@ class ApiUserForm extends Component {
name
} = apiUser
+ // We display validation for a particular field on blur (after the user finishes typing in it),
+ // so it is not too disruptive to the user.
+ // The onBlur/onHandleBlur and touched props are used to that effect.
+ // All field validation errors are also shown when the user clicks Create Account.
+
return (
{createUser &&
Sign up for API access
}
-
-
- I have read and consent to the{' '}
- Terms of Service{' '}
- for using the {process.env.API_NAME}.
- >
+
+ {({
+ handleBlur,
+ handleSubmit,
+ handleChange,
+ values,
+ touched,
+ isValid,
+ errors
+ }) => (
+
+ You must agree to the terms to continue.
-
- {createUser &&
-
- }
-
+
+ )}
+
+
)
}
diff --git a/components/LayoutWithAuth0.js b/components/LayoutWithAuth0.js
index d11a8fd..fda622f 100644
--- a/components/LayoutWithAuth0.js
+++ b/components/LayoutWithAuth0.js
@@ -59,8 +59,20 @@ class LayoutWithAuth0 extends Component {
isUserRequested: true
})
// TODO: Combine into a single fetch fromToken or use SWR
- const adminUser = await secureFetch(`${ADMIN_USER_URL}/fromtoken`, accessToken)
- const apiUser = await secureFetch(`${API_USER_URL}/fromtoken`, accessToken)
+ const adminUserFetchResult = await secureFetch(`${ADMIN_USER_URL}/fromtoken`, accessToken)
+ const apiUserFetchResult = await secureFetch(`${API_USER_URL}/fromtoken`, accessToken)
+
+ // Check that the contents of the fetch result for admin user and api user is valid
+ // This means for instance checking for existence of a data.id field.
+ // If the user was not found, something else is returned of the form
+ // data: {
+ // "result": "ERR",
+ // "message": "No user with id=000000 found.",
+ // "code": 404,
+ // "detail": null
+ // }
+ const adminUser = adminUserFetchResult.data.id ? adminUserFetchResult.data : null
+ const apiUser = apiUserFetchResult.data.id ? apiUserFetchResult.data : null
this.setState({
...state,
diff --git a/package.json b/package.json
index 6cc8be6..1638d06 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
"bootstrap": "^4.5.0",
"clone": "^2.1.2",
"dotenv": "^8.2.0",
+ "formik": "^2.1.5",
"isomorphic-unfetch": "^3.0.0",
"moment": "^2.24.0",
"next": "^9.3.2",
@@ -25,7 +26,8 @@
"styled-components": "^5.0.1",
"styled-icons": "^10.2.1",
"swr": "^0.3.2",
- "use-auth0-hooks": "^0.7.0"
+ "use-auth0-hooks": "^0.7.0",
+ "yup": "^0.29.3"
},
"devDependencies": {
"mastarm": "^5.3.1",
diff --git a/util/middleware.js b/util/middleware.js
index 9452784..878173f 100644
--- a/util/middleware.js
+++ b/util/middleware.js
@@ -44,7 +44,8 @@ export async function createOrUpdateUser (url, userData, isNew, accessToken) {
}
// TODO: improve the UI feedback messages for this.
- if (result.status === 'success' && result.data) {
+ // A successful call has the user record (with id) in the data field.
+ if (result.data.id) {
return result.data
} else {
alert(`An error was encountered:\n${JSON.stringify(result)}`)
diff --git a/yarn.lock b/yarn.lock
index 0e1785b..a416f01 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4256,6 +4256,11 @@ deep-is@~0.1.3:
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
+deepmerge@^2.1.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170"
+ integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==
+
default-gateway@^2.6.0:
version "2.7.2"
resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-2.7.2.tgz#b7ef339e5e024b045467af403d50348db4642d0f"
@@ -5343,6 +5348,11 @@ flush-write-stream@^1.0.0:
inherits "^2.0.3"
readable-stream "^2.3.6"
+fn-name@~3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/fn-name/-/fn-name-3.0.0.tgz#0596707f635929634d791f452309ab41558e3c5c"
+ integrity sha512-eNMNr5exLoavuAMhIUVsOKF79SWd/zG104ef6sxBTSw+cZc6BXdQXDvYcGvp0VbxVVSp1XDUNoz7mg1xMtSznA==
+
for-in@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
@@ -5362,6 +5372,20 @@ form-data@~2.3.2:
combined-stream "^1.0.6"
mime-types "^2.1.12"
+formik@^2.1.5:
+ version "2.1.5"
+ resolved "https://registry.yarnpkg.com/formik/-/formik-2.1.5.tgz#de5bbbe35543fa6d049fe96b8ee329d6cd6892b8"
+ integrity sha512-bWpo3PiqVDYslvrRjTq0Isrm0mFXHiO33D8MS6t6dWcqSFGeYF52nlpCM2xwOJ6tRVRznDkL+zz/iHPL4LDuvQ==
+ dependencies:
+ deepmerge "^2.1.1"
+ hoist-non-react-statics "^3.3.0"
+ lodash "^4.17.14"
+ lodash-es "^4.17.14"
+ react-fast-compare "^2.0.1"
+ scheduler "^0.18.0"
+ tiny-warning "^1.0.2"
+ tslib "^1.10.0"
+
fragment-cache@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
@@ -5767,7 +5791,7 @@ hoek@4.2.1:
resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb"
integrity sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==
-hoist-non-react-statics@^3.0.0:
+hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.3.0:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@@ -7154,7 +7178,7 @@ locate-path@^5.0.0:
dependencies:
p-locate "^4.1.0"
-lodash-es@^4.17.15:
+lodash-es@^4.17.11, lodash-es@^4.17.14, lodash-es@^4.17.15:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78"
integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==
@@ -9398,6 +9422,11 @@ prop-types@15.7.2, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2, p
object-assign "^4.1.1"
react-is "^16.8.1"
+property-expr@^2.0.2:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.4.tgz#37b925478e58965031bb612ec5b3260f8241e910"
+ integrity sha512-sFPkHQjVKheDNnPvotjQmm3KD3uk1fWKUN7CrpdbwmUx3CrG3QiM8QpTSimvig5vTXmTvjz7+TDvXOI9+4rkcg==
+
prr@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
@@ -9576,6 +9605,11 @@ react-dom@^16.13.0:
prop-types "^15.6.2"
scheduler "^0.19.1"
+react-fast-compare@^2.0.1:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
+ integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
+
react-input-autosize@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-2.2.2.tgz#fcaa7020568ec206bc04be36f4eb68e647c4d8c2"
@@ -10236,6 +10270,14 @@ sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4:
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
+scheduler@^0.18.0:
+ version "0.18.0"
+ resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.18.0.tgz#5901ad6659bc1d8f3fdaf36eb7a67b0d6746b1c4"
+ integrity sha512-agTSHR1Nbfi6ulI0kYNK0203joW2Y5W4po4l+v03tOoiJKpTBbxpNhWDvqc/4IcOw+KLmSiQLTasZ4cab2/UWQ==
+ dependencies:
+ loose-envify "^1.1.0"
+ object-assign "^4.1.1"
+
scheduler@^0.19.1:
version "0.19.1"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196"
@@ -11092,6 +11134,11 @@ symbol-tree@^3.2.2:
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
+synchronous-promise@^2.0.13:
+ version "2.0.13"
+ resolved "https://registry.yarnpkg.com/synchronous-promise/-/synchronous-promise-2.0.13.tgz#9d8c165ddee69c5a6542862b405bc50095926702"
+ integrity sha512-R9N6uDkVsghHePKh1TEqbnLddO2IY25OcsksyFp/qBe7XYd0PVbKEWxhcdMhpLzE1I6skj5l4aEZ3CRxcbArlA==
+
syntax-error@^1.1.1:
version "1.4.0"
resolved "https://registry.yarnpkg.com/syntax-error/-/syntax-error-1.4.0.tgz#2d9d4ff5c064acb711594a3e3b95054ad51d907c"
@@ -11241,6 +11288,11 @@ timsort@^0.3.0:
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
+tiny-warning@^1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
+ integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
+
tmp@^0.0.33:
version "0.0.33"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
@@ -11300,6 +11352,11 @@ toidentifier@1.0.0:
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
+toposort@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330"
+ integrity sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=
+
tough-cookie@^2.3.3, tough-cookie@^2.3.4, tough-cookie@~2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
@@ -11350,7 +11407,7 @@ tsconfig-paths@^3.9.0:
minimist "^1.2.0"
strip-bom "^3.0.0"
-tslib@^1.9.0:
+tslib@^1.10.0, tslib@^1.9.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==
@@ -12078,3 +12135,16 @@ yargs@^13.2.4, yargs@^13.3.0:
which-module "^2.0.0"
y18n "^4.0.0"
yargs-parser "^13.1.2"
+
+yup@^0.29.3:
+ version "0.29.3"
+ resolved "https://registry.yarnpkg.com/yup/-/yup-0.29.3.tgz#69a30fd3f1c19f5d9e31b1cf1c2b851ce8045fea"
+ integrity sha512-RNUGiZ/sQ37CkhzKFoedkeMfJM0vNQyaz+wRZJzxdKE7VfDeVKH8bb4rr7XhRLbHJz5hSjoDNwMEIaKhuMZ8gQ==
+ dependencies:
+ "@babel/runtime" "^7.10.5"
+ fn-name "~3.0.0"
+ lodash "^4.17.15"
+ lodash-es "^4.17.11"
+ property-expr "^2.0.2"
+ synchronous-promise "^2.0.13"
+ toposort "^2.0.2"
From 622baa4f5ccaa7c54edb2325ceb06e4e81f76f70 Mon Sep 17 00:00:00 2001
From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com>
Date: Thu, 10 Sep 2020 11:28:28 -0400
Subject: [PATCH 2/4] refactor(ApiUserForm): Remove old state mgmt.
---
components/ApiUserForm.js | 72 +++++++++++----------------------------
1 file changed, 19 insertions(+), 53 deletions(-)
diff --git a/components/ApiUserForm.js b/components/ApiUserForm.js
index e454875..52a3bb5 100644
--- a/components/ApiUserForm.js
+++ b/components/ApiUserForm.js
@@ -7,7 +7,7 @@ import * as yup from 'yup'
import { AUTH0_SCOPE } from '../util/constants'
-// The validation schema for the form.
+// The validation schema for the form fields.
const validationSchema = yup.object({
appName: yup.string().required('Please enter your application name.'),
appPurpose: yup.string(),
@@ -17,6 +17,20 @@ const validationSchema = yup.object({
name: yup.string().required('Please enter your name.')
})
+/**
+ * Creates a blank ApiUser object to be filled out.
+ */
+function createBlankApiUser () {
+ return {
+ appName: '',
+ appPurpose: '',
+ appUrl: '',
+ company: '',
+ hasConsentedToTerms: false,
+ name: ''
+ }
+}
+
/**
* The basic form for creating an ApiUser, including input validation.
* This can also be used to show a disabled view of the form (for viewing user details).
@@ -24,30 +38,6 @@ const validationSchema = yup.object({
* TODO: Add the ability to update a user?
*/
class ApiUserForm extends Component {
- constructor (props) {
- super(props)
- this.state = {
- apiUser: {
- appName: '',
- appPurpose: '',
- appUrl: '',
- company: '',
- hasConsentedToTerms: false,
- name: ''
- }
- }
- }
-
- handleChange = field => e => {
- const newData = {}
- newData[field] = e.target.value
- this.updateUserState(newData)
- }
-
- handleTermsChange = e => {
- this.updateUserState({ hasConsentedToTerms: e.target.checked })
- }
-
handleCreateAccount = async apiUserData => {
const { auth, createUser } = this.props
if (auth.user) {
@@ -63,45 +53,22 @@ class ApiUserForm extends Component {
}
}
- updateUserState = newUserData => {
- const { apiUser } = this.state
- this.setState({
- apiUser: {
- ...apiUser,
- ...newUserData
- }
- })
- }
-
- dummy () {
-
- }
-
render () {
const { createUser } = this.props
- // Default values to apiUser passed from props. Otherwise, use original state.
- // It is assumed that if coming from props, the apiUser already exists.
- const apiUser = this.props.apiUser || this.state.apiUser
- const {
- appName,
- appPurpose,
- appUrl,
- company,
- hasConsentedToTerms,
- name
- } = apiUser
+ // If the ApiUser already exists, it is passed from props.
+ // Otherwise, it is a new ApiUser, and a blank one is created.
+ const apiUser = this.props.apiUser || createBlankApiUser()
// We display validation for a particular field on blur (after the user finishes typing in it),
// so it is not too disruptive to the user.
// The onBlur/onHandleBlur and touched props are used to that effect.
// All field validation errors are also shown when the user clicks Create Account.
-
return (