Browse Source

fix create member validation

Klaudiusz Dembler 3 years ago
parent
commit
f9ccbe63cf

+ 3 - 2
package.json

@@ -70,6 +70,7 @@
     "@types/react-transition-group": "^4.4.0",
     "@types/video.js": "^7.3.10",
     "apollo": "^2.30.2",
+    "awesome-debounce-promise": "^2.1.0",
     "axios": "^0.21.1",
     "bn.js": "~5.2.0",
     "body-scroll-lock": "^3.1.5",
@@ -122,14 +123,14 @@
     "webpack-merge": "^5.7.3"
   },
   "devDependencies": {
-    "@storybook/addons": "^6.1.16",
     "@storybook/addon-actions": "^6.1.16",
     "@storybook/addon-essentials": "^6.1.16",
     "@storybook/addon-links": "^6.1.16",
+    "@storybook/addons": "^6.1.16",
     "@storybook/node-logger": "^6.1.16",
     "@storybook/preset-create-react-app": "^3.1.5",
-    "@storybook/theming": "^6.1.16",
     "@storybook/react": "^6.1.16",
+    "@storybook/theming": "^6.1.16",
     "@svgr/cli": "^5.5.0"
   },
   "browserslist": {

+ 3 - 3
src/utils/formValidationOptions.ts

@@ -36,17 +36,17 @@ export const textFieldValidation = ({
   },
   minLength: {
     value: minLength,
-    message: `${name} must be at least ${minLength} characters.`,
+    message: `${name} must be at least ${minLength} characters`,
   },
   maxLength: {
     value: maxLength,
-    message: `${name} cannot be longer than ${maxLength} characters.`,
+    message: `${name} cannot be longer than ${maxLength} characters`,
   },
   ...(pattern
     ? {
         pattern: {
           value: pattern,
-          message: patternMessage ? `${name} ${patternMessage}` : `${name} must be a valid`,
+          message: patternMessage ? `${name} ${patternMessage}` : `${name} must be valid`,
         },
       }
     : {}),

+ 49 - 48
src/views/studio/CreateMemberView/CreateMemberView.tsx

@@ -4,8 +4,8 @@ import { useUser, useConnectionStatus } from '@/hooks'
 import { Spinner } from '@/shared/components'
 import TextArea from '@/shared/components/TextArea'
 import { textFieldValidation } from '@/utils/formValidationOptions'
-import { debounce } from 'lodash'
-import React, { useEffect, useState } from 'react'
+import debouncePromise from 'awesome-debounce-promise'
+import React, { useEffect, useRef, useState } from 'react'
 import { useForm } from 'react-hook-form'
 import { useNavigate } from 'react-router'
 import { FAUCET_URL } from '@/config/urls'
@@ -19,11 +19,13 @@ import {
   StyledAvatar,
   StyledTextField,
 } from './CreateMemberView.style'
-import { useQueryNodeStateSubscription, useMembership } from '@/api/hooks'
+import { useQueryNodeStateSubscription } from '@/api/hooks'
 
 import axios, { AxiosError } from 'axios'
 import { MemberId } from '@/joystream-lib'
 import { MEMBERSHIP_NAME_PATTERN, URL_PATTERN } from '@/config/regex'
+import { useApolloClient } from '@apollo/client'
+import { GetMembershipDocument, GetMembershipQuery, GetMembershipQueryVariables } from '@/api/queries'
 
 type Inputs = {
   handle: string
@@ -36,7 +38,7 @@ const CreateMemberView = () => {
   const { nodeConnectionStatus } = useConnectionStatus()
 
   const navigate = useNavigate()
-  const { register, handleSubmit, errors, trigger, watch } = useForm<Inputs>({
+  const { register, handleSubmit, errors } = useForm<Inputs>({
     shouldFocusError: false,
     defaultValues: {
       handle: '',
@@ -55,9 +57,7 @@ const CreateMemberView = () => {
     throw queryNodeStateError
   }
 
-  const { refetch: refetchMember } = useMembership({
-    where: { handle: watch('handle') },
-  })
+  const client = useApolloClient()
 
   // success
   useEffect(() => {
@@ -90,17 +90,41 @@ const CreateMemberView = () => {
     }
   })
 
-  const debounceAvatarChange = debounce(async (value) => {
-    await trigger('avatar')
-    if (!errors.avatar) {
-      setAvatarImageUrl(value)
-    }
-  }, 500)
+  const debouncedHandleAvatarChange = useRef(debouncePromise((value: string) => setAvatarImageUrl(value), 500))
+
+  const debouncedAvatarValidation = useRef(
+    debouncePromise(
+      async (value: string): Promise<string | boolean> =>
+        new Promise((resolve) => {
+          if (!value) resolve(true)
+          const img = new Image()
+          img.src = value
+          img.onerror = () => resolve('Image not found')
+          img.onload = () => {
+            setAvatarImageUrl(value)
+            resolve(true)
+          }
+        }),
+      500
+    )
+  )
+
+  const debouncedHandleUniqueValidation = useRef(
+    debouncePromise(async (value: string) => {
+      const {
+        data: { membershipByUniqueInput },
+      } = await client.query<GetMembershipQuery, GetMembershipQueryVariables>({
+        query: GetMembershipDocument,
+        variables: { where: { handle: value } },
+      })
+      if (membershipByUniqueInput) {
+        return 'Member handle already in use'
+      } else {
+        return true
+      }
+    }, 500)
+  )
 
-  const handleAvatarChange = (e: React.ChangeEvent<HTMLInputElement>) => {
-    const value = e.currentTarget.value
-    debounceAvatarChange(value)
-  }
   return (
     <Wrapper>
       <Header>
@@ -114,7 +138,7 @@ const CreateMemberView = () => {
         <StyledAvatar size="view" imageUrl={errors.avatar ? undefined : avatarImageUrl} />
         <StyledTextField
           name="avatar"
-          onChange={handleAvatarChange}
+          onChange={(e) => debouncedHandleAvatarChange.current(e.target.value)}
           label="Avatar URL"
           placeholder="https://example.com/avatar.jpeg"
           ref={register(
@@ -124,17 +148,7 @@ const CreateMemberView = () => {
               patternMessage: 'must be a valid url',
               maxLength: 200,
               required: false,
-              validate: async (value) => {
-                if (!value) return
-                return new Promise((resolve) => {
-                  debounce(async (handle) => {
-                    const img = new Image()
-                    img.src = value
-                    img.onerror = () => resolve('Image not found')
-                    img.onload = () => resolve(true)
-                  }, 500)(value)
-                })
-              },
+              validate: debouncedAvatarValidation.current,
             })
           )}
           error={!!errors.avatar}
@@ -143,35 +157,22 @@ const CreateMemberView = () => {
         <StyledTextField
           name="handle"
           placeholder="johnnysmith"
-          label="Member Name"
+          label="Member handle"
           ref={register(
             textFieldValidation({
-              name: 'Member name',
+              name: 'Member handle',
               maxLength: 40,
               minLength: 4,
               required: true,
               pattern: MEMBERSHIP_NAME_PATTERN,
               patternMessage: 'may contain only lowercase letters, numbers and underscores',
-              validate: async (value) => {
-                // Wrapping it up with promise and resolving comparison inside debounce
-                // debounce() will not automatically do the return, which is needed for validation
-                return new Promise((resolve) => {
-                  debounce(async (handle) => {
-                    const {
-                      data: { membershipByUniqueInput },
-                    } = await refetchMember()
-                    if (membershipByUniqueInput) {
-                      resolve('Member name is already taken')
-                    } else {
-                      resolve(true)
-                    }
-                  }, 500)(value)
-                })
-              },
+              validate: debouncedHandleUniqueValidation.current,
             })
           )}
           error={!!errors.handle}
-          helperText={errors.handle?.message}
+          helperText={
+            errors.handle?.message || 'Member handle may contain only lowercase letters, numbers and underscores'
+          }
         />
         <TextArea
           name="about"

+ 32 - 0
yarn.lock

@@ -4322,6 +4322,11 @@
   dependencies:
     cropperjs "*"
 
+"@types/debounce-promise@^3.1.1":
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/@types/debounce-promise/-/debounce-promise-3.1.3.tgz#dd0d6b96ee61da0dd4c413e3ea03a425ffa36b3f"
+  integrity sha512-mjcCf//DAUQ6YLQMhqYJAv/+a4BsE1GQFmy1el5K62wLJJmQwGi3TsnshhOFynPpuBF9Gh2Vvb+5ImPi47KaZw==
+
 "@types/eslint-visitor-keys@^1.0.0":
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
@@ -6011,6 +6016,28 @@ await-to-js@^2.0.1:
   resolved "https://registry.yarnpkg.com/await-to-js/-/await-to-js-2.1.1.tgz#c2093cd5a386f2bb945d79b292817bbc3f41b31b"
   integrity sha512-CHBC6gQGCIzjZ09tJ+XmpQoZOn4GdWePB4qUweCaKNJ0D3f115YdhmYVTZ4rMVpiJ3cFzZcTYK1VMYEICV4YXw==
 
+awesome-debounce-promise@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/awesome-debounce-promise/-/awesome-debounce-promise-2.1.0.tgz#7c21cadf2eaa1fa0a29449dd4dfdbd5004811e53"
+  integrity sha512-0Dv4j2wKk5BrNZh4jgV2HUdznaeVgEK/WTvcHhZWUElhmQ1RR+iURRoLEwICFyR0S/5VtxfcvY6gR+qSe95jNg==
+  dependencies:
+    "@types/debounce-promise" "^3.1.1"
+    awesome-imperative-promise "^1.0.1"
+    awesome-only-resolves-last-promise "^1.0.3"
+    debounce-promise "^3.1.0"
+
+awesome-imperative-promise@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/awesome-imperative-promise/-/awesome-imperative-promise-1.0.1.tgz#be143d89615b9110ac310345457f37e9791ddac9"
+  integrity sha512-EmPr3FqbQGqlNh+WxMNcF9pO9uDQJnOC4/3rLBQNH9m4E9qI+8lbfHCmHpVAsmGqPJPKhCjJLHUQzQW/RBHRdQ==
+
+awesome-only-resolves-last-promise@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/awesome-only-resolves-last-promise/-/awesome-only-resolves-last-promise-1.0.3.tgz#f2dac3ff7df238e48b818b423483031750f13ef0"
+  integrity sha512-7q4WPsYiD8Omvi/yHL314DkvsD/lM//Z2/KcU1vWk0xJotiV0GMJTgHTpWl3n90HJqpXKg7qX+VVNs5YbQyPRQ==
+  dependencies:
+    awesome-imperative-promise "^1.0.1"
+
 aws-sign2@~0.7.0:
   version "0.7.0"
   resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
@@ -8478,6 +8505,11 @@ de-indent@^1.0.2:
   resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
   integrity sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0=
 
+debounce-promise@^3.1.0:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/debounce-promise/-/debounce-promise-3.1.2.tgz#320fb8c7d15a344455cd33cee5ab63530b6dc7c5"
+  integrity sha512-rZHcgBkbYavBeD9ej6sP56XfG53d51CD4dnaw989YX/nZ/ZJfgRx/9ePKmTNiUiyQvh4mtrMoS3OAWW+yoYtpg==
+
 debounce@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.0.tgz#44a540abc0ea9943018dc0eaa95cce87f65cd131"