Bladeren bron

feat: add max length validation for access token description in multiple languages

reiji-h 1 jaar geleden
bovenliggende
commit
588fe2d4be

+ 1 - 0
apps/app/public/static/locales/en_US/translation.json

@@ -255,6 +255,7 @@
       "title": "Create New Access Token",
       "expiredAt_desc": "Select when this access token should expire.",
       "description_desc": "Provide a description to help you identify this token later",
+      "description_max_length": "Description must be less than {{length}} characters",
       "scope_desc": "Select the scope of the access token."
     },
     "copy_to_clipboard": "Copy to clipboard"

+ 1 - 0
apps/app/public/static/locales/fr_FR/translation.json

@@ -256,6 +256,7 @@
       "title": "Créer un nouveau jeton d'accès",
       "expiredAt_desc": "Sélectionnez la date d'expiration de ce jeton d'accès.",
       "description_desc": "Fournissez une description pour vous aider à identifier ce jeton ultérieurement.",
+      "description_max_length": "Veuillez saisir jusqu'à {{length}} caractères."
       "scope_desc": "Sélectionnez la portée du jeton d'accès."
     },
     "copy_to_clipboard": "Copier dans le presse-papiers"

+ 1 - 0
apps/app/public/static/locales/ja_JP/translation.json

@@ -256,6 +256,7 @@
       "title": "アクセストークンを作成",
       "expiredAt_desc": "アクセストークンの有効期限を選択します。",
       "description_desc": "このトークンを後で識別するための説明を入力します。",
+      "description_max_length": "{{length}}文字以内で入力してください。",
       "scope_desc": "スコープによって、このトークンで行える操作を制限します。"
     },
     "copy_to_clipboard": "Copy to clipboard"

+ 1 - 0
apps/app/public/static/locales/zh_CN/translation.json

@@ -263,6 +263,7 @@
       "title": "创建新访问令牌",
       "expiredAt_desc": "选择此访问令牌的过期时间。",
       "description_desc": "提供描述以帮助您稍后识别此令牌。",
+      "description_max_length": "请输入最多 {{length}} 个字符",
       "scope_desc": "选择访问令牌的范围。"
     },
     "copy_to_clipboard": "复制到剪贴板"

+ 55 - 18
apps/app/src/client/components/Me/AccessTokenForm.tsx

@@ -1,7 +1,8 @@
-import type { FormEventHandler } from 'react';
 import React from 'react';
 
 import { useTranslation } from 'next-i18next';
+import { useForm } from 'react-hook-form';
+
 
 import type { IAccessTokenInfo } from '~/interfaces/access-token';
 
@@ -11,6 +12,13 @@ type AccessTokenFormProps = {
   submitHandler: (info: IAccessTokenInfo) => Promise<void>;
 }
 
+type FormInputs = {
+  expiredAt: string;
+  description: string;
+  // TODO: Implement scope selection
+  // scopes: string[];
+}
+
 // TODO: Implement scope selection
 export const AccessTokenForm = React.memo((props: AccessTokenFormProps): JSX.Element => {
   const { submitHandler } = props;
@@ -18,17 +26,27 @@ export const AccessTokenForm = React.memo((props: AccessTokenFormProps): JSX.Ele
 
   const defaultExpiredAt = new Date();
   defaultExpiredAt.setMonth(defaultExpiredAt.getMonth() + 1);
+  const defaultExpiredAtStr = defaultExpiredAt.toISOString().split('T')[0];
+  const todayStr = new Date().toISOString().split('T')[0];
 
-  const handleSubmit: FormEventHandler<HTMLFormElement> = (e) => {
-    e.preventDefault();
-    const form = new FormData(e.currentTarget);
-    const expiredAtDate = new Date(form.get('expiredAt') as string);
-    const description = form.get('description') as string;
+  const {
+    register,
+    handleSubmit,
+    formState: { errors, isValid },
+  } = useForm<FormInputs>({
+    defaultValues: {
+      expiredAt: defaultExpiredAtStr,
+      description: '',
+    },
+  });
+
+  const onSubmit = (data: FormInputs) => {
+    const expiredAtDate = new Date(data.expiredAt);
     const scope = []; // TODO: Implement scope selection
 
     submitHandler({
       expiredAt: expiredAtDate,
-      description,
+      description: data.description,
       scope,
     });
   };
@@ -37,7 +55,7 @@ export const AccessTokenForm = React.memo((props: AccessTokenFormProps): JSX.Ele
     <div className="card mt-3 mb-4">
       <div className="card-header">{t('page_me_access_token.form.title')}</div>
       <div className="card-body">
-        <form onSubmit={handleSubmit}>
+        <form onSubmit={handleSubmit(onSubmit)}>
           <div className="mb-3">
             <label htmlFor="expiredAt" className="form-label">{t('page_me_access_token.expiredAt')}</label>
             <div className="row">
@@ -45,14 +63,24 @@ export const AccessTokenForm = React.memo((props: AccessTokenFormProps): JSX.Ele
                 <div className="input-group">
                   <input
                     type="date"
-                    className="form-control"
+                    className={`form-control ${errors.expiredAt ? 'is-invalid' : ''}`}
                     data-testid="grw-accesstoken-input-expiredAt"
-                    name="expiredAt"
-                    min={new Date().toISOString().split('T')[0]}
-                    required
-                    defaultValue={defaultExpiredAt.toISOString().split('T')[0]}
+                    min={todayStr}
+                    {...register('expiredAt', {
+                      required: t('input_validation.message.required', { param: t('page_me_access_token.expiredAt') }),
+                      validate: (value) => {
+                        const date = new Date(value);
+                        const now = new Date();
+                        return date > now || 'Expiration date must be in the future';
+                      },
+                    })}
                   />
                 </div>
+                {errors.expiredAt && (
+                  <div className="invalid-feedback d-block">
+                    {errors.expiredAt.message}
+                  </div>
+                )}
               </div>
             </div>
             <div className="form-text">{t('page_me_access_token.form.expiredAt_desc')}</div>
@@ -61,14 +89,22 @@ export const AccessTokenForm = React.memo((props: AccessTokenFormProps): JSX.Ele
           <div className="mb-3">
             <label htmlFor="description" className="form-label">{t('page_me_access_token.description')}</label>
             <textarea
-              className="form-control"
-              name="description"
-              maxLength={MAX_DESCRIPTION_LENGTH}
+              className={`form-control ${errors.description ? 'is-invalid' : ''}`}
               rows={3}
-              required
-              defaultValue=""
               data-testid="grw-accesstoken-textarea-description"
+              {...register('description', {
+                required: t('input_validation.message.required', { param: t('page_me_access_token.description') }),
+                maxLength: {
+                  value: MAX_DESCRIPTION_LENGTH,
+                  message: t('page_me_access_token.form.description_max_length', { length: MAX_DESCRIPTION_LENGTH }),
+                },
+              })}
             />
+            {errors.description && (
+              <div className="invalid-feedback">
+                {errors.description.message}
+              </div>
+            )}
             <div className="form-text">{t('page_me_access_token.form.description_desc')}</div>
           </div>
 
@@ -82,6 +118,7 @@ export const AccessTokenForm = React.memo((props: AccessTokenFormProps): JSX.Ele
             type="submit"
             className="btn btn-primary"
             data-testid="grw-accesstoken-create-button"
+            disabled={!isValid}
           >
             {t('page_me_access_token.create_token')}
           </button>