InstallerForm.tsx 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. import type { FormEventHandler, JSX } from 'react';
  2. import { memo, useCallback, useState } from 'react';
  3. import { Lang, AllLang } from '@growi/core';
  4. import { LoadingSpinner } from '@growi/ui/dist/components';
  5. import { useTranslation } from 'next-i18next';
  6. import { useRouter } from 'next/router';
  7. import { i18n as i18nConfig } from '^/config/next-i18next.config';
  8. import { apiv3Post } from '~/client/util/apiv3-client';
  9. import { useTWithOpt } from '~/client/util/t-with-opt';
  10. import { toastError } from '~/client/util/toastr';
  11. import type { IErrorV3 } from '~/interfaces/errors/v3-error';
  12. import styles from './InstallerForm.module.scss';
  13. const moduleClass = styles['installer-form'] ?? '';
  14. type Props = {
  15. minPasswordLength: number,
  16. }
  17. const InstallerForm = memo((props: Props): JSX.Element => {
  18. const { t, i18n } = useTranslation();
  19. const { minPasswordLength } = props;
  20. const router = useRouter();
  21. const tWithOpt = useTWithOpt();
  22. const isSupportedLang = AllLang.includes(i18n.language as Lang);
  23. const [isLoading, setIsLoading] = useState(false);
  24. const [currentLocale, setCurrentLocale] = useState(isSupportedLang ? i18n.language : Lang.en_US);
  25. const [registerErrors, setRegisterErrors] = useState<IErrorV3[]>([]);
  26. const onClickLanguageItem = useCallback((locale) => {
  27. i18n.changeLanguage(locale);
  28. setCurrentLocale(locale);
  29. }, [i18n]);
  30. const submitHandler: FormEventHandler = useCallback(async(e: any) => {
  31. e.preventDefault();
  32. setIsLoading(true);
  33. const formData = e.target.elements;
  34. const {
  35. 'registerForm[username]': { value: username },
  36. 'registerForm[name]': { value: name },
  37. 'registerForm[email]': { value: email },
  38. 'registerForm[password]': { value: password },
  39. } = formData;
  40. const data = {
  41. registerForm: {
  42. username,
  43. name,
  44. email,
  45. password,
  46. 'app:globalLang': currentLocale,
  47. },
  48. };
  49. try {
  50. setRegisterErrors([]);
  51. await apiv3Post('/installer', data);
  52. router.push('/');
  53. }
  54. catch (errs) {
  55. const err = errs[0];
  56. const code = err.code;
  57. setIsLoading(false);
  58. setRegisterErrors(errs);
  59. if (code === 'failed_to_login_after_install') {
  60. toastError(t('installer.failed_to_login_after_install'));
  61. setTimeout(() => { router.push('/login') }, 700); // Wait 700 ms to show toastr
  62. }
  63. toastError(t('installer.failed_to_install'));
  64. }
  65. }, [currentLocale, router, t]);
  66. return (
  67. <div data-testid="installerForm" className={`${moduleClass} nologin-dialog py-3 px-4 rounded-4 rounded-top-0 mx-auto`}>
  68. <div className="row mt-3">
  69. <div className="col-md-12">
  70. <p className="alert alert-success">
  71. <strong>{ t('installer.create_initial_account') }</strong><br />
  72. <small>{ t('installer.initial_account_will_be_administrator_automatically') }</small>
  73. </p>
  74. </div>
  75. </div>
  76. <div className="row mt-2">
  77. {
  78. registerErrors != null && registerErrors.length > 0 && (
  79. <div className="col-12">
  80. <div className="alert alert-danger text-center">
  81. {registerErrors.map(err => (
  82. <span>
  83. {tWithOpt(err.message, err.args)}<br />
  84. </span>
  85. ))}
  86. </div>
  87. </div>
  88. )
  89. }
  90. <form role="form" id="register-form" className="ps-1" onSubmit={submitHandler}>
  91. <div className="dropdown mb-3">
  92. <div className="input-group dropdown-with-icon">
  93. <span className="p-2 text-white opacity-75">
  94. <span className="material-symbols-outlined">language</span>
  95. </span>
  96. <button
  97. type="button"
  98. className="btn btn-secondary dropdown-toggle form-control text-end rounded"
  99. id="dropdownLanguage"
  100. data-testid="dropdownLanguage"
  101. data-bs-toggle="dropdown"
  102. aria-haspopup="true"
  103. aria-expanded="true"
  104. >
  105. <span className="float-start">
  106. {t('meta.display_name')}
  107. </span>
  108. </button>
  109. <input
  110. type="hidden"
  111. name="registerForm[app:globalLang]"
  112. />
  113. <div className="dropdown-menu" aria-labelledby="dropdownLanguage">
  114. {
  115. i18nConfig.locales.map((locale) => {
  116. let fixedT;
  117. if (i18n != null) {
  118. fixedT = i18n.getFixedT(locale);
  119. i18n.loadLanguages(i18nConfig.locales);
  120. }
  121. return (
  122. <button
  123. key={locale}
  124. data-testid={`dropdownLanguageMenu-${locale}`}
  125. className="dropdown-item"
  126. type="button"
  127. onClick={() => { onClickLanguageItem(locale) }}
  128. >
  129. {fixedT?.('meta.display_name')}
  130. </button>
  131. );
  132. })
  133. }
  134. </div>
  135. </div>
  136. </div>
  137. <div className="input-group mb-3">
  138. <label className="p-2 text-white opacity-75" aria-label={t('User ID')} htmlFor="tiUsername">
  139. <span className="material-symbols-outlined" aria-hidden>person</span>
  140. </label>
  141. <input
  142. id="tiUsername"
  143. type="text"
  144. className="form-control rounded"
  145. placeholder={t('User ID')}
  146. name="registerForm[username]"
  147. required
  148. />
  149. </div>
  150. <div className="input-group mb-3">
  151. <label className="p-2 text-white opacity-75" aria-label={t('Name')} htmlFor="tiName">
  152. <span className="material-symbols-outlined" aria-hidden>sell</span>
  153. </label>
  154. <input
  155. id="tiName"
  156. type="text"
  157. className="form-control rounded"
  158. placeholder={t('Name')}
  159. name="registerForm[name]"
  160. required
  161. />
  162. </div>
  163. <div className="input-group mb-3">
  164. <label className="p-2 text-white opacity-75" aria-label={t('Email')} htmlFor="tiEmail">
  165. <span className="material-symbols-outlined" aria-hidden>mail</span>
  166. </label>
  167. <input
  168. id="tiEmail"
  169. type="email"
  170. className="form-control rounded"
  171. placeholder={t('Email')}
  172. name="registerForm[email]"
  173. required
  174. />
  175. </div>
  176. <div className="input-group mb-3">
  177. <label className="p-2 text-white opacity-75" aria-label={t('Password')} htmlFor="tiPassword">
  178. <span className="material-symbols-outlined" aria-hidden>lock</span>
  179. </label>
  180. <input
  181. minLength={minPasswordLength}
  182. id="tiPassword"
  183. type="password"
  184. className="form-control rounded"
  185. placeholder={t('Password')}
  186. name="registerForm[password]"
  187. required
  188. />
  189. </div>
  190. <div className="input-group mt-4 justify-content-center">
  191. <button
  192. type="submit"
  193. className="btn btn-secondary btn-register col-6 d-flex"
  194. disabled={isLoading}
  195. >
  196. <span aria-hidden>
  197. {isLoading ? (
  198. <LoadingSpinner />
  199. ) : (
  200. <span className="material-symbols-outlined">person_add</span>
  201. )}
  202. </span>
  203. <span className="flex-grow-1">{ t('Create') }</span>
  204. </button>
  205. </div>
  206. <div>
  207. <a href="https://growi.org" className="link-growi-org">
  208. <span className="growi">GROWI</span><span className="org">.org</span>
  209. </a>
  210. </div>
  211. </form>
  212. </div>
  213. </div>
  214. );
  215. });
  216. InstallerForm.displayName = 'InstallerForm';
  217. export default InstallerForm;