InstallerForm.tsx 8.9 KB

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