Yuki Takei 5 месяцев назад
Родитель
Сommit
d1c129688f

+ 50 - 29
apps/app/src/client/components/Admin/App/AppSetting.jsx

@@ -1,7 +1,8 @@
-import React, { useCallback } from 'react';
+import React, { useCallback, useEffect } from 'react';
 
 
 import { useTranslation, i18n } from 'next-i18next';
 import { useTranslation, i18n } from 'next-i18next';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
+import { useForm } from 'react-hook-form';
 
 
 import { i18n as i18nConfig } from '^/config/next-i18next.config';
 import { i18n as i18nConfig } from '^/config/next-i18next.config';
 
 
@@ -20,8 +21,44 @@ const AppSetting = (props) => {
   const { adminAppContainer } = props;
   const { adminAppContainer } = props;
   const { t } = useTranslation(['admin', 'commons']);
   const { t } = useTranslation(['admin', 'commons']);
 
 
-  const submitHandler = useCallback(async() => {
+  const {
+    register,
+    handleSubmit,
+    reset,
+  } = useForm();
+
+  // Reset form when adminAppContainer state changes (e.g., after reload)
+  useEffect(() => {
+    reset({
+      title: adminAppContainer.state.title || '',
+      confidential: adminAppContainer.state.confidential || '',
+      globalLang: adminAppContainer.state.globalLang || 'en-US',
+      // Convert boolean to string for radio button value
+      isEmailPublishedForNewUser: String(adminAppContainer.state.isEmailPublishedForNewUser ?? true),
+      fileUpload: adminAppContainer.state.fileUpload ?? false,
+    });
+  }, [
+    adminAppContainer.state.title,
+    adminAppContainer.state.confidential,
+    adminAppContainer.state.globalLang,
+    adminAppContainer.state.isEmailPublishedForNewUser,
+    adminAppContainer.state.fileUpload,
+    reset,
+  ]);
+
+  const onSubmit = useCallback(async(data) => {
     try {
     try {
+      // Await all setState completions before API call
+      await Promise.all([
+        adminAppContainer.changeTitle(data.title),
+        adminAppContainer.changeConfidential(data.confidential),
+        adminAppContainer.changeGlobalLang(data.globalLang),
+      ]);
+      // Convert string 'true'/'false' to boolean
+      const isEmailPublished = data.isEmailPublishedForNewUser === 'true' || data.isEmailPublishedForNewUser === true;
+      await adminAppContainer.changeIsEmailPublishedForNewUserShow(isEmailPublished);
+      await adminAppContainer.changeFileUpload(data.fileUpload);
+
       await adminAppContainer.updateAppSettingHandler();
       await adminAppContainer.updateAppSettingHandler();
       toastSuccess(t('commons:toaster.update_successed', { target: t('commons:headers.app_settings') }));
       toastSuccess(t('commons:toaster.update_successed', { target: t('commons:headers.app_settings') }));
     }
     }
@@ -33,18 +70,15 @@ const AppSetting = (props) => {
 
 
 
 
   return (
   return (
-    <React.Fragment>
+    <form onSubmit={handleSubmit(onSubmit)}>
       <div className="row">
       <div className="row">
         <label className="text-start text-md-end col-md-3 col-form-label">{t('admin:app_setting.site_name')}</label>
         <label className="text-start text-md-end col-md-3 col-form-label">{t('admin:app_setting.site_name')}</label>
         <div className="col-md-6">
         <div className="col-md-6">
           <input
           <input
             className="form-control"
             className="form-control"
             type="text"
             type="text"
-            value={adminAppContainer.state.title || ''}
-            onChange={(e) => {
-              adminAppContainer.changeTitle(e.target.value);
-            }}
             placeholder="GROWI"
             placeholder="GROWI"
+            {...register('title')}
           />
           />
           <p className="form-text text-muted">{t('admin:app_setting.sitename_change')}</p>
           <p className="form-text text-muted">{t('admin:app_setting.sitename_change')}</p>
         </div>
         </div>
@@ -60,11 +94,8 @@ const AppSetting = (props) => {
           <input
           <input
             className="form-control"
             className="form-control"
             type="text"
             type="text"
-            value={adminAppContainer.state.confidential || ''}
-            onChange={(e) => {
-              adminAppContainer.changeConfidential(e.target.value);
-            }}
             placeholder={t('admin:app_setting.confidential_example')}
             placeholder={t('admin:app_setting.confidential_example')}
+            {...register('confidential')}
           />
           />
           <p className="form-text text-muted">{t('admin:app_setting.header_content')}</p>
           <p className="form-text text-muted">{t('admin:app_setting.header_content')}</p>
         </div>
         </div>
@@ -88,12 +119,8 @@ const AppSetting = (props) => {
                     type="radio"
                     type="radio"
                     id={`radioLang${locale}`}
                     id={`radioLang${locale}`}
                     className="form-check-input"
                     className="form-check-input"
-                    name="globalLang"
                     value={locale}
                     value={locale}
-                    checked={adminAppContainer.state.globalLang === locale}
-                    onChange={(e) => {
-                      adminAppContainer.changeGlobalLang(e.target.value);
-                    }}
+                    {...register('globalLang')}
                   />
                   />
                   <label className="form-label form-check-label" htmlFor={`radioLang${locale}`}>{fixedT('meta.display_name')}</label>
                   <label className="form-label form-check-label" htmlFor={`radioLang${locale}`}>{fixedT('meta.display_name')}</label>
                 </div>
                 </div>
@@ -116,9 +143,8 @@ const AppSetting = (props) => {
               type="radio"
               type="radio"
               id="radio-email-show"
               id="radio-email-show"
               className="form-check-input"
               className="form-check-input"
-              name="mailVisibility"
-              checked={adminAppContainer.state.isEmailPublishedForNewUser === true}
-              onChange={() => { adminAppContainer.changeIsEmailPublishedForNewUserShow(true) }}
+              value="true"
+              {...register('isEmailPublishedForNewUser')}
             />
             />
             <label className="form-label form-check-label" htmlFor="radio-email-show">{t('commons:Show')}</label>
             <label className="form-label form-check-label" htmlFor="radio-email-show">{t('commons:Show')}</label>
           </div>
           </div>
@@ -128,9 +154,8 @@ const AppSetting = (props) => {
               type="radio"
               type="radio"
               id="radio-email-hide"
               id="radio-email-hide"
               className="form-check-input"
               className="form-check-input"
-              name="mailVisibility"
-              checked={adminAppContainer.state.isEmailPublishedForNewUser === false}
-              onChange={() => { adminAppContainer.changeIsEmailPublishedForNewUserShow(false) }}
+              value="false"
+              {...register('isEmailPublishedForNewUser')}
             />
             />
             <label className="form-label form-check-label" htmlFor="radio-email-hide">{t('commons:Hide')}</label>
             <label className="form-label form-check-label" htmlFor="radio-email-hide">{t('commons:Hide')}</label>
           </div>
           </div>
@@ -150,11 +175,7 @@ const AppSetting = (props) => {
               type="checkbox"
               type="checkbox"
               id="cbFileUpload"
               id="cbFileUpload"
               className="form-check-input"
               className="form-check-input"
-              name="fileUpload"
-              checked={adminAppContainer.state.fileUpload}
-              onChange={(e) => {
-                adminAppContainer.changeFileUpload(e.target.checked);
-              }}
+              {...register('fileUpload')}
             />
             />
             <label
             <label
               className="form-label form-check-label"
               className="form-label form-check-label"
@@ -170,8 +191,8 @@ const AppSetting = (props) => {
         </div>
         </div>
       </div>
       </div>
 
 
-      <AdminUpdateButtonRow onClick={submitHandler} disabled={adminAppContainer.state.retrieveError != null} />
-    </React.Fragment>
+      <AdminUpdateButtonRow type="submit" disabled={adminAppContainer.state.retrieveError != null} />
+    </form>
   );
   );
 
 
 };
 };

+ 56 - 17
apps/app/src/client/components/Admin/App/MailSetting.tsx

@@ -1,14 +1,15 @@
-import React from 'react';
+import React, { useCallback, useEffect } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
+import { useForm } from 'react-hook-form';
 
 
 import AdminAppContainer from '~/client/services/AdminAppContainer';
 import AdminAppContainer from '~/client/services/AdminAppContainer';
 import { toastSuccess, toastError } from '~/client/util/toastr';
 import { toastSuccess, toastError } from '~/client/util/toastr';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
 
-import SesSetting from './SesSetting';
-import SmtpSetting from './SmtpSetting';
+import { SesSetting } from './SesSetting';
+import { SmtpSetting } from './SmtpSetting';
 
 
 
 
 type Props = {
 type Props = {
@@ -22,15 +23,57 @@ const MailSetting = (props: Props) => {
 
 
   const transmissionMethods = ['smtp', 'ses'];
   const transmissionMethods = ['smtp', 'ses'];
 
 
-  async function submitHandler() {
+  const {
+    register,
+    handleSubmit,
+    reset,
+  } = useForm();
+
+  // Reset form when adminAppContainer state changes
+  useEffect(() => {
+    reset({
+      fromAddress: adminAppContainer.state.fromAddress || '',
+      transmissionMethod: adminAppContainer.state.transmissionMethod || 'smtp',
+      smtpHost: adminAppContainer.state.smtpHost || '',
+      smtpPort: adminAppContainer.state.smtpPort || '',
+      smtpUser: adminAppContainer.state.smtpUser || '',
+      smtpPassword: adminAppContainer.state.smtpPassword || '',
+      sesAccessKeyId: adminAppContainer.state.sesAccessKeyId || '',
+      sesSecretAccessKey: adminAppContainer.state.sesSecretAccessKey || '',
+    });
+  }, [
+    adminAppContainer.state.fromAddress,
+    adminAppContainer.state.transmissionMethod,
+    adminAppContainer.state.smtpHost,
+    adminAppContainer.state.smtpPort,
+    adminAppContainer.state.smtpUser,
+    adminAppContainer.state.smtpPassword,
+    adminAppContainer.state.sesAccessKeyId,
+    adminAppContainer.state.sesSecretAccessKey,
+    reset,
+  ]);
+
+  const onSubmit = useCallback(async(data) => {
     try {
     try {
+      // Await all setState completions before API call
+      await Promise.all([
+        adminAppContainer.changeFromAddress(data.fromAddress),
+        adminAppContainer.changeTransmissionMethod(data.transmissionMethod),
+        adminAppContainer.changeSmtpHost(data.smtpHost),
+        adminAppContainer.changeSmtpPort(data.smtpPort),
+        adminAppContainer.changeSmtpUser(data.smtpUser),
+        adminAppContainer.changeSmtpPassword(data.smtpPassword),
+        adminAppContainer.changeSesAccessKeyId(data.sesAccessKeyId),
+        adminAppContainer.changeSesSecretAccessKey(data.sesSecretAccessKey),
+      ]);
+
       await adminAppContainer.updateMailSettingHandler();
       await adminAppContainer.updateMailSettingHandler();
       toastSuccess(t('toaster.update_successed', { target: t('admin:app_setting.mail_settings'), ns: 'commons' }));
       toastSuccess(t('toaster.update_successed', { target: t('admin:app_setting.mail_settings'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
     }
     }
-  }
+  }, [adminAppContainer, t]);
 
 
   async function sendTestEmailHandler() {
   async function sendTestEmailHandler() {
     const { adminAppContainer } = props;
     const { adminAppContainer } = props;
@@ -45,7 +88,7 @@ const MailSetting = (props: Props) => {
 
 
 
 
   return (
   return (
-    <React.Fragment>
+    <form onSubmit={handleSubmit(onSubmit)}>
       {!adminAppContainer.state.isMailerSetup && (
       {!adminAppContainer.state.isMailerSetup && (
         <div className="alert alert-danger"><span className="material-symbols-outlined">error</span> {t('admin:app_setting.mailer_is_not_set_up')}</div>
         <div className="alert alert-danger"><span className="material-symbols-outlined">error</span> {t('admin:app_setting.mailer_is_not_set_up')}</div>
       )}
       )}
@@ -56,8 +99,7 @@ const MailSetting = (props: Props) => {
             className="form-control"
             className="form-control"
             type="text"
             type="text"
             placeholder={`${t('eg')} mail@growi.org`}
             placeholder={`${t('eg')} mail@growi.org`}
-            value={adminAppContainer.state.fromAddress || ''}
-            onChange={(e) => { adminAppContainer.changeFromAddress(e.target.value) }}
+            {...register('fromAddress')}
           />
           />
         </div>
         </div>
       </div>
       </div>
@@ -73,12 +115,9 @@ const MailSetting = (props: Props) => {
                 <input
                 <input
                   type="radio"
                   type="radio"
                   className="form-check-input"
                   className="form-check-input"
-                  name="transmission-method"
                   id={`transmission-method-radio-${method}`}
                   id={`transmission-method-radio-${method}`}
-                  checked={adminAppContainer.state.transmissionMethod === method}
-                  onChange={(e) => {
-                    adminAppContainer.changeTransmissionMethod(method);
-                  }}
+                  value={method}
+                  {...register('transmissionMethod')}
                 />
                 />
                 <label className="form-label form-check-label" htmlFor={`transmission-method-radio-${method}`}>{t(`admin:app_setting.${method}_label`)}</label>
                 <label className="form-label form-check-label" htmlFor={`transmission-method-radio-${method}`}>{t(`admin:app_setting.${method}_label`)}</label>
               </div>
               </div>
@@ -87,12 +126,12 @@ const MailSetting = (props: Props) => {
         </div>
         </div>
       </div>
       </div>
 
 
-      {adminAppContainer.state.transmissionMethod === 'smtp' && <SmtpSetting />}
-      {adminAppContainer.state.transmissionMethod === 'ses' && <SesSetting />}
+      {adminAppContainer.state.transmissionMethod === 'smtp' && <SmtpSetting register={register} />}
+      {adminAppContainer.state.transmissionMethod === 'ses' && <SesSetting register={register} />}
 
 
       <div className="row my-3">
       <div className="row my-3">
         <div className="mx-auto">
         <div className="mx-auto">
-          <button type="button" className="btn btn-primary" onClick={submitHandler} disabled={adminAppContainer.state.retrieveError != null}>
+          <button type="submit" className="btn btn-primary" disabled={adminAppContainer.state.retrieveError != null}>
             { t('Update') }
             { t('Update') }
           </button>
           </button>
           {adminAppContainer.state.transmissionMethod === 'smtp' && (
           {adminAppContainer.state.transmissionMethod === 'smtp' && (
@@ -102,7 +141,7 @@ const MailSetting = (props: Props) => {
           )}
           )}
         </div>
         </div>
       </div>
       </div>
-    </React.Fragment>
+    </form>
   );
   );
 
 
 };
 };

+ 14 - 14
apps/app/src/client/components/Admin/App/SesSetting.tsx

@@ -1,20 +1,24 @@
 
 
 import React from 'react';
 import React from 'react';
 
 
+import type { UseFormRegister } from 'react-hook-form';
+
 import AdminAppContainer from '~/client/services/AdminAppContainer';
 import AdminAppContainer from '~/client/services/AdminAppContainer';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
 
 type Props = {
 type Props = {
-  adminAppContainer: AdminAppContainer,
+  adminAppContainer?: AdminAppContainer,
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  register: UseFormRegister<any>,
 }
 }
 
 
-const SmtpSetting = (props: Props) => {
-  const { adminAppContainer } = props;
+const SesSetting = (props: Props): JSX.Element => {
+  const { register } = props;
 
 
   return (
   return (
     <React.Fragment>
     <React.Fragment>
-      <div id="mail-smtp" className="tab-pane active mt-5">
+      <div id="mail-ses" className="tab-pane active mt-5">
 
 
         <div className="row">
         <div className="row">
           <label className="text-start text-md-end col-md-3 col-form-label">
           <label className="text-start text-md-end col-md-3 col-form-label">
@@ -24,10 +28,7 @@ const SmtpSetting = (props: Props) => {
             <input
             <input
               className="form-control"
               className="form-control"
               type="text"
               type="text"
-              value={adminAppContainer.state.sesAccessKeyId || ''}
-              onChange={(e) => {
-                adminAppContainer.changeSesAccessKeyId(e.target.value);
-              }}
+              {...register('sesAccessKeyId')}
             />
             />
           </div>
           </div>
         </div>
         </div>
@@ -40,10 +41,7 @@ const SmtpSetting = (props: Props) => {
             <input
             <input
               className="form-control"
               className="form-control"
               type="text"
               type="text"
-              value={adminAppContainer.state.sesSecretAccessKey || ''}
-              onChange={(e) => {
-                adminAppContainer.changeSesSecretAccessKey(e.target.value);
-              }}
+              {...register('sesSecretAccessKey')}
             />
             />
           </div>
           </div>
         </div>
         </div>
@@ -53,9 +51,11 @@ const SmtpSetting = (props: Props) => {
   );
   );
 };
 };
 
 
+export { SesSetting };
+
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const SmtpSettingWrapper = withUnstatedContainers(SmtpSetting, [AdminAppContainer]);
+const SesSettingWrapper = withUnstatedContainers(SesSetting, [AdminAppContainer]);
 
 
-export default SmtpSettingWrapper;
+export default SesSettingWrapper;

+ 22 - 9
apps/app/src/client/components/Admin/App/SiteUrlSetting.tsx

@@ -1,6 +1,7 @@
-import React, { useCallback } from 'react';
+import React, { useCallback, useEffect } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
+import { useForm } from 'react-hook-form';
 
 
 import AdminAppContainer from '~/client/services/AdminAppContainer';
 import AdminAppContainer from '~/client/services/AdminAppContainer';
 import { toastSuccess, toastError } from '~/client/util/toastr';
 import { toastSuccess, toastError } from '~/client/util/toastr';
@@ -21,9 +22,23 @@ const SiteUrlSetting = (props: Props) => {
   const { t: tCommon } = useTranslation('commons');
   const { t: tCommon } = useTranslation('commons');
   const { adminAppContainer } = props;
   const { adminAppContainer } = props;
 
 
+  const {
+    register,
+    handleSubmit,
+    reset,
+  } = useForm();
 
 
-  const submitHandler = useCallback(async() => {
+  // Reset form when adminAppContainer state changes
+  useEffect(() => {
+    reset({
+      siteUrl: adminAppContainer.state.siteUrl || '',
+    });
+  }, [adminAppContainer.state.siteUrl, reset]);
+
+  const onSubmit = useCallback(async(data) => {
     try {
     try {
+      // Await setState completion before API call
+      await adminAppContainer.changeSiteUrl(data.siteUrl);
       await adminAppContainer.updateSiteUrlSettingHandler();
       await adminAppContainer.updateSiteUrlSettingHandler();
       toastSuccess(tCommon('toaster.update_successed', { target: t('site_url.title') }));
       toastSuccess(tCommon('toaster.update_successed', { target: t('site_url.title') }));
     }
     }
@@ -34,7 +49,7 @@ const SiteUrlSetting = (props: Props) => {
   }, [adminAppContainer, t, tCommon]);
   }, [adminAppContainer, t, tCommon]);
 
 
   return (
   return (
-    <React.Fragment>
+    <form onSubmit={handleSubmit(onSubmit)}>
       <p className="card custom-card bg-body-tertiary">{t('site_url.desc')}</p>
       <p className="card custom-card bg-body-tertiary">{t('site_url.desc')}</p>
       {!adminAppContainer.state.isSetSiteUrl
       {!adminAppContainer.state.isSetSiteUrl
           && (<p className="alert alert-danger"><span className="material-symbols-outlined">error</span> {t('site_url.warn')}</p>)}
           && (<p className="alert alert-danger"><span className="material-symbols-outlined">error</span> {t('site_url.warn')}</p>)}
@@ -69,11 +84,9 @@ const SiteUrlSetting = (props: Props) => {
                 <input
                 <input
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
-                  name="settingForm[app:siteUrl]"
-                  value={adminAppContainer.state.siteUrl || ''}
-                  disabled={adminAppContainer.state.siteUrlUseOnlyEnvVars ?? true}
-                  onChange={(e) => { adminAppContainer.changeSiteUrl(e.target.value) }}
+                  readOnly={adminAppContainer.state.siteUrlUseOnlyEnvVars ?? true}
                   placeholder="e.g. https://my.growi.org"
                   placeholder="e.g. https://my.growi.org"
+                  {...register('siteUrl')}
                 />
                 />
                 <p className="form-text text-muted">
                 <p className="form-text text-muted">
                   {/* eslint-disable-next-line react/no-danger */}
                   {/* eslint-disable-next-line react/no-danger */}
@@ -92,8 +105,8 @@ const SiteUrlSetting = (props: Props) => {
         </table>
         </table>
       </div>
       </div>
 
 
-      <AdminUpdateButtonRow onClick={submitHandler} disabled={adminAppContainer.state.retrieveError != null} />
-    </React.Fragment>
+      <AdminUpdateButtonRow type="submit" disabled={adminAppContainer.state.retrieveError != null} />
+    </form>
   );
   );
 };
 };
 
 

+ 12 - 11
apps/app/src/client/components/Admin/App/SmtpSetting.tsx

@@ -2,6 +2,7 @@
 import React from 'react';
 import React from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
+import type { UseFormRegister } from 'react-hook-form';
 
 
 import AdminAppContainer from '~/client/services/AdminAppContainer';
 import AdminAppContainer from '~/client/services/AdminAppContainer';
 
 
@@ -9,12 +10,14 @@ import { withUnstatedContainers } from '../../UnstatedUtils';
 
 
 
 
 type Props = {
 type Props = {
-  adminAppContainer: AdminAppContainer,
+  adminAppContainer?: AdminAppContainer,
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  register: UseFormRegister<any>,
 }
 }
 
 
-const SmtpSetting = (props: Props) => {
+const SmtpSetting = (props: Props): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
-  const { adminAppContainer } = props;
+  const { register } = props;
 
 
   return (
   return (
     <React.Fragment>
     <React.Fragment>
@@ -27,8 +30,7 @@ const SmtpSetting = (props: Props) => {
             <input
             <input
               className="form-control"
               className="form-control"
               type="text"
               type="text"
-              value={adminAppContainer.state.smtpHost || ''}
-              onChange={(e) => { adminAppContainer.changeSmtpHost(e.target.value) }}
+              {...register('smtpHost')}
             />
             />
           </div>
           </div>
         </div>
         </div>
@@ -40,8 +42,7 @@ const SmtpSetting = (props: Props) => {
           <div className="col-md-6">
           <div className="col-md-6">
             <input
             <input
               className="form-control"
               className="form-control"
-              value={adminAppContainer.state.smtpPort || ''}
-              onChange={(e) => { adminAppContainer.changeSmtpPort(e.target.value) }}
+              {...register('smtpPort')}
             />
             />
           </div>
           </div>
         </div>
         </div>
@@ -54,8 +55,7 @@ const SmtpSetting = (props: Props) => {
             <input
             <input
               className="form-control"
               className="form-control"
               type="text"
               type="text"
-              value={adminAppContainer.state.smtpUser || ''}
-              onChange={(e) => { adminAppContainer.changeSmtpUser(e.target.value) }}
+              {...register('smtpUser')}
             />
             />
           </div>
           </div>
         </div>
         </div>
@@ -68,8 +68,7 @@ const SmtpSetting = (props: Props) => {
             <input
             <input
               className="form-control"
               className="form-control"
               type="password"
               type="password"
-              value={adminAppContainer.state.smtpPassword || ''}
-              onChange={(e) => { adminAppContainer.changeSmtpPassword(e.target.value) }}
+              {...register('smtpPassword')}
             />
             />
           </div>
           </div>
         </div>
         </div>
@@ -78,6 +77,8 @@ const SmtpSetting = (props: Props) => {
   );
   );
 };
 };
 
 
+export { SmtpSetting };
+
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */

+ 11 - 2
apps/app/src/client/components/Admin/Common/AdminUpdateButtonRow.tsx

@@ -3,8 +3,9 @@ import React, { type JSX } from 'react';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
 type Props = {
 type Props = {
-  onClick: () => void,
+  onClick?: () => void,
   disabled?: boolean,
   disabled?: boolean,
+  type?: 'button' | 'submit' | 'reset',
 }
 }
 
 
 const AdminUpdateButtonRow = (props: Props): JSX.Element => {
 const AdminUpdateButtonRow = (props: Props): JSX.Element => {
@@ -13,7 +14,15 @@ const AdminUpdateButtonRow = (props: Props): JSX.Element => {
   return (
   return (
     <div className="row my-3">
     <div className="row my-3">
       <div className="mx-auto">
       <div className="mx-auto">
-        <button type="button" className="btn btn-primary" onClick={props.onClick} disabled={props.disabled ?? false}>{ t('Update') }</button>
+        <button
+          // eslint-disable-next-line react/button-has-type
+          type={props.type ?? 'button'}
+          className="btn btn-primary"
+          onClick={props.onClick}
+          disabled={props.disabled ?? false}
+        >
+          { t('Update') }
+        </button>
       </div>
       </div>
     </div>
     </div>
   );
   );