InstallerForm.tsx 8.6 KB

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