InstallerForm.tsx 7.9 KB

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