InstallerForm.tsx 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. import type { FormEventHandler } 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 { toastError } from '~/client/util/toastr';
  10. import type { IErrorV3 } from '~/interfaces/errors/v3-error';
  11. import styles from './InstallerForm.module.scss';
  12. const moduleClass = styles['installer-form'] ?? '';
  13. const InstallerForm = memo((): JSX.Element => {
  14. const { t, i18n } = useTranslation();
  15. const router = useRouter();
  16. const isSupportedLang = AllLang.includes(i18n.language as Lang);
  17. const [isValidUserName, setValidUserName] = useState(true);
  18. const [isLoading, setIsLoading] = useState(false);
  19. const [currentLocale, setCurrentLocale] = useState(isSupportedLang ? i18n.language : Lang.en_US);
  20. const [registerErrors, setRegisterErrors] = useState<IErrorV3[]>([]);
  21. const checkUserName = useCallback(async(event) => {
  22. const axios = require('axios').create({
  23. headers: {
  24. 'Content-Type': 'application/json',
  25. 'X-Requested-With': 'XMLHttpRequest',
  26. },
  27. responseType: 'json',
  28. });
  29. const res = await axios.get('/_api/v3/check-username', { params: { username: event.target.value } });
  30. setValidUserName(res.data.valid);
  31. }, []);
  32. const onClickLanguageItem = useCallback((locale) => {
  33. i18n.changeLanguage(locale);
  34. setCurrentLocale(locale);
  35. }, [i18n]);
  36. const submitHandler: FormEventHandler = useCallback(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. }
  60. catch (errs) {
  61. const err = errs[0];
  62. const code = err.code;
  63. setIsLoading(false);
  64. setRegisterErrors(errs);
  65. if (code === 'failed_to_login_after_install') {
  66. toastError(t('installer.failed_to_login_after_install'));
  67. setTimeout(() => { router.push('/login') }, 700); // Wait 700 ms to show toastr
  68. }
  69. toastError(t('installer.failed_to_install'));
  70. }
  71. }, [currentLocale, router, t]);
  72. const hasErrorClass = isValidUserName ? '' : ' has-error';
  73. const unavailableUserId = isValidUserName
  74. ? ''
  75. : <span><span className="material-symbols-outlined">block</span>{ t('installer.unavaliable_user_id') }</span>;
  76. return (
  77. <div data-testid="installerForm" className={`${moduleClass} nologin-dialog py-3 px-4 rounded-4 rounded-top-0 mx-auto${hasErrorClass}`}>
  78. <div className="row mt-3">
  79. <div className="col-md-12">
  80. <p className="alert alert-success">
  81. <strong>{ t('installer.create_initial_account') }</strong><br />
  82. <small>{ t('installer.initial_account_will_be_administrator_automatically') }</small>
  83. </p>
  84. </div>
  85. </div>
  86. <div className="row mt-2">
  87. {
  88. registerErrors != null && registerErrors.length > 0 && (
  89. <p className="alert alert-danger text-center">
  90. {registerErrors.map(err => (
  91. <span>
  92. {t(err.message)}<br />
  93. </span>
  94. ))}
  95. </p>
  96. )
  97. }
  98. <form role="form" id="register-form" className="ps-1" onSubmit={submitHandler}>
  99. <div className="dropdown mb-3">
  100. <div className="input-group dropdown-with-icon">
  101. <span className="p-2 text-white opacity-75">
  102. <span className="material-symbols-outlined">language</span>
  103. </span>
  104. <button
  105. type="button"
  106. className="btn btn-secondary dropdown-toggle form-control text-end rounded"
  107. id="dropdownLanguage"
  108. data-testid="dropdownLanguage"
  109. data-bs-toggle="dropdown"
  110. aria-haspopup="true"
  111. aria-expanded="true"
  112. >
  113. <span className="float-start">
  114. {t('meta.display_name')}
  115. </span>
  116. </button>
  117. <input
  118. type="hidden"
  119. name="registerForm[app:globalLang]"
  120. />
  121. <div className="dropdown-menu" aria-labelledby="dropdownLanguage">
  122. {
  123. i18nConfig.locales.map((locale) => {
  124. let fixedT;
  125. if (i18n != null) {
  126. fixedT = i18n.getFixedT(locale);
  127. i18n.loadLanguages(i18nConfig.locales);
  128. }
  129. return (
  130. <button
  131. key={locale}
  132. data-testid={`dropdownLanguageMenu-${locale}`}
  133. className="dropdown-item"
  134. type="button"
  135. onClick={() => { onClickLanguageItem(locale) }}
  136. >
  137. {fixedT?.('meta.display_name')}
  138. </button>
  139. );
  140. })
  141. }
  142. </div>
  143. </div>
  144. </div>
  145. <div className={`input-group mb-3${hasErrorClass}`}>
  146. <span className="p-2 text-white opacity-75">
  147. <span className="material-symbols-outlined">person</span>
  148. </span>
  149. <input
  150. data-testid="tiUsername"
  151. type="text"
  152. className="form-control rounded"
  153. placeholder={t('User ID')}
  154. name="registerForm[username]"
  155. // onBlur={checkUserName} // need not to check username before installation -- 2020.07.24 Yuki Takei
  156. required
  157. />
  158. </div>
  159. <p className="form-text">{ unavailableUserId }</p>
  160. <div className="input-group mb-3">
  161. <span className="p-2 text-white opacity-75">
  162. <span className="material-symbols-outlined">sell</span>
  163. </span>
  164. <input
  165. data-testid="tiName"
  166. type="text"
  167. className="form-control rounded"
  168. placeholder={t('Name')}
  169. name="registerForm[name]"
  170. required
  171. />
  172. </div>
  173. <div className="input-group mb-3">
  174. <span className="p-2 text-white opacity-75">
  175. <span className="material-symbols-outlined">mail</span>
  176. </span>
  177. <input
  178. data-testid="tiEmail"
  179. type="email"
  180. className="form-control rounded"
  181. placeholder={t('Email')}
  182. name="registerForm[email]"
  183. required
  184. />
  185. </div>
  186. <div className="input-group mb-3">
  187. <span className="p-2 text-white opacity-75">
  188. <span className="material-symbols-outlined">lock</span>
  189. </span>
  190. <input
  191. data-testid="tiPassword"
  192. type="password"
  193. className="form-control rounded"
  194. placeholder={t('Password')}
  195. name="registerForm[password]"
  196. required
  197. />
  198. </div>
  199. <div className="input-group mt-4 justify-content-center">
  200. <button
  201. data-testid="btnSubmit"
  202. type="submit"
  203. className="btn btn-secondary btn-register col-6 d-flex"
  204. disabled={isLoading}
  205. >
  206. <span>
  207. {isLoading ? (
  208. <LoadingSpinner />
  209. ) : (
  210. <span className="material-symbols-outlined">person_add</span>
  211. )}
  212. </span>
  213. <span className="flex-grow-1">{ t('Create') }</span>
  214. </button>
  215. </div>
  216. <div>
  217. <a href="https://growi.org" className="link-growi-org">
  218. <span className="growi">GROWI</span><span className="org">.org</span>
  219. </a>
  220. </div>
  221. </form>
  222. </div>
  223. </div>
  224. );
  225. });
  226. InstallerForm.displayName = 'InstallerForm';
  227. export default InstallerForm;