UserInviteModal.jsx 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. import React from 'react';
  2. import { useAtomValue } from 'jotai';
  3. import { useTranslation } from 'next-i18next';
  4. import PropTypes from 'prop-types';
  5. import { CopyToClipboard } from 'react-copy-to-clipboard';
  6. // import Button from 'react-bootstrap/es/Button';
  7. import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
  8. import AdminUsersContainer from '~/client/services/AdminUsersContainer';
  9. import { toastError, toastSuccess, toastWarning } from '~/client/util/toastr';
  10. import { isMailerSetupAtom } from '~/states/server-configurations';
  11. import { withUnstatedContainers } from '../../UnstatedUtils';
  12. class UserInviteModal extends React.Component {
  13. constructor(props) {
  14. super(props);
  15. this.state = {
  16. emailInputValue: '',
  17. sendEmail: false,
  18. invitedEmailList: null,
  19. isCreateUserButtonPushed: false,
  20. };
  21. this.handleSubmit = this.handleSubmit.bind(this);
  22. this.handleInput = this.handleInput.bind(this);
  23. this.handleCheckBox = this.handleCheckBox.bind(this);
  24. this.onToggleModal = this.onToggleModal.bind(this);
  25. }
  26. onToggleModal() {
  27. this.props.adminUsersContainer.toggleUserInviteModal();
  28. this.setState({ invitedEmailList: null });
  29. }
  30. showToaster() {
  31. toastSuccess('Copied Mail and Password');
  32. }
  33. showToasterByEmailList(emailList, toast) {
  34. let msg = '';
  35. emailList.forEach((email) => {
  36. msg += `・${email}<br>`;
  37. });
  38. switch (toast) {
  39. case 'success':
  40. msg = `User has been created<br>${msg}`;
  41. toastSuccess(msg);
  42. break;
  43. case 'warning':
  44. msg = `Existing email<br>${msg}`;
  45. toastWarning(msg);
  46. break;
  47. case 'error':
  48. toastError({ message: msg });
  49. break;
  50. }
  51. }
  52. renderModalBody() {
  53. const { t } = this.props;
  54. return (
  55. <>
  56. <label className="form-label" htmlFor="admin-invite-emails">
  57. {t('admin:user_management.invite_modal.emails')}
  58. </label>
  59. <p>
  60. {t('admin:user_management.invite_modal.description1')}
  61. <br />
  62. {t('admin:user_management.invite_modal.description2')}
  63. </p>
  64. <textarea
  65. className="form-control"
  66. id="admin-invite-emails"
  67. placeholder="e.g.&#13;&#10;user1@growi.org&#13;&#10;user2@growi.org"
  68. style={{ height: '200px' }}
  69. value={this.state.emailInputValue}
  70. onChange={this.handleInput}
  71. />
  72. {!this.validEmail() && (
  73. <p className="m-2 text-danger">
  74. {t('admin:user_management.invite_modal.valid_email')}
  75. </p>
  76. )}
  77. </>
  78. );
  79. }
  80. renderCreatedModalBody() {
  81. const { t } = this.props;
  82. const { invitedEmailList } = this.state;
  83. return (
  84. <>
  85. <p>{t('admin:user_management.invite_modal.temporary_password')}</p>
  86. <p>{t('admin:user_management.invite_modal.send_new_password')}</p>
  87. {invitedEmailList.createdUserList.length > 0 &&
  88. this.renderCreatedEmail(invitedEmailList.createdUserList)}
  89. {invitedEmailList.existingEmailList.length > 0 &&
  90. this.renderExistingEmail(invitedEmailList.existingEmailList)}
  91. </>
  92. );
  93. }
  94. renderModalFooter() {
  95. const { t, isMailerSetup } = this.props;
  96. const { isCreateUserButtonPushed } = this.state;
  97. return (
  98. <>
  99. <div
  100. className="col text-start form-check form-check-info"
  101. onChange={this.handleCheckBox}
  102. >
  103. <input
  104. type="checkbox"
  105. id="sendEmail"
  106. className="form-check-input"
  107. name="sendEmail"
  108. defaultChecked={this.state.sendEmail}
  109. disabled={!isMailerSetup}
  110. />
  111. <label className="form-label form-check-label" htmlFor="sendEmail">
  112. {t('admin:user_management.invite_modal.invite_thru_email')}
  113. </label>
  114. {isMailerSetup ? (
  115. <p
  116. className="form-text text-muted"
  117. // biome-ignore lint/security/noDangerouslySetInnerHtml: includes markup from i18n strings
  118. dangerouslySetInnerHTML={{
  119. __html: t(
  120. 'admin:user_management.invite_modal.mail_setting_link',
  121. ),
  122. }}
  123. />
  124. ) : (
  125. <p
  126. className="form-text text-muted"
  127. // biome-ignore lint/security/noDangerouslySetInnerHtml: includes markup from i18n strings
  128. dangerouslySetInnerHTML={{
  129. __html: t('admin:mailer_setup_required'),
  130. }}
  131. />
  132. )}
  133. </div>
  134. <div>
  135. <button
  136. type="button"
  137. className="btn btn-outline-secondary me-2"
  138. onClick={this.onToggleModal}
  139. >
  140. {t('Cancel')}
  141. </button>
  142. <button
  143. type="button"
  144. className="btn btn-primary"
  145. onClick={this.handleSubmit}
  146. disabled={!this.validEmail() || isCreateUserButtonPushed}
  147. >
  148. {t('admin:user_management.invite_modal.issue')}
  149. </button>
  150. </div>
  151. </>
  152. );
  153. }
  154. renderCreatedModalFooter() {
  155. const { t } = this.props;
  156. return (
  157. <>
  158. <div className="form-label me-3 text-start" style={{ flex: 1 }}>
  159. <text className="text-danger">
  160. {t('admin:user_management.invite_modal.send_temporary_password')}
  161. </text>
  162. <text>{t('admin:user_management.invite_modal.send_email')}</text>
  163. </div>
  164. <button
  165. type="button"
  166. className="btn btn-outline-secondary"
  167. onClick={this.onToggleModal}
  168. >
  169. {t('Close')}
  170. </button>
  171. </>
  172. );
  173. }
  174. renderCreatedEmail(userList) {
  175. return (
  176. <ul>
  177. {userList.map((user) => {
  178. const copyText = `Email:${user.email} Password:${user.password}`;
  179. return (
  180. <div className="my-1" key={user.email}>
  181. <CopyToClipboard text={copyText} onCopy={this.showToaster}>
  182. <li className="btn btn-outline-secondary">
  183. Email: <strong className="me-3">{user.email}</strong>{' '}
  184. Password: <strong>{user.password}</strong>
  185. </li>
  186. </CopyToClipboard>
  187. </div>
  188. );
  189. })}
  190. </ul>
  191. );
  192. }
  193. renderExistingEmail(emailList) {
  194. const { t } = this.props;
  195. return (
  196. <>
  197. <p className="text-warning">
  198. {t('admin:user_management.invite_modal.existing_email')}
  199. </p>
  200. <ul>
  201. {emailList.map((user) => {
  202. return (
  203. <li key={user}>
  204. <strong>{user}</strong>
  205. </li>
  206. );
  207. })}
  208. </ul>
  209. </>
  210. );
  211. }
  212. validEmail() {
  213. return this.state.emailInputValue.match(/.+@.+\..+/) != null;
  214. }
  215. async handleSubmit() {
  216. const { adminUsersContainer } = this.props;
  217. this.setState({ isCreateUserButtonPushed: true });
  218. const array = this.state.emailInputValue.split('\n');
  219. const emailList = array.filter((element) => {
  220. return element.match(/.+@.+\..+/);
  221. });
  222. const shapedEmailList = emailList.map((email) => {
  223. return email.trim();
  224. });
  225. try {
  226. const emailList = await adminUsersContainer.createUserInvited(
  227. shapedEmailList,
  228. this.state.sendEmail,
  229. );
  230. this.setState({ emailInputValue: '' });
  231. this.setState({ invitedEmailList: emailList });
  232. if (emailList.createdUserList.length > 0) {
  233. const createdEmailList = emailList.createdUserList.map((user) => {
  234. return user.email;
  235. });
  236. this.showToasterByEmailList(createdEmailList, 'success');
  237. }
  238. if (emailList.existingEmailList.length > 0) {
  239. this.showToasterByEmailList(emailList.existingEmailList, 'warning');
  240. }
  241. if (emailList.failedEmailList.length > 0) {
  242. const failedEmailList = emailList.failedEmailList.map(
  243. (failed, index) => {
  244. let messgage = `email: ${failed.email}<br>・reason: ${failed.reason}`;
  245. if (index !== emailList.failedEmailList.length - 1) {
  246. messgage += '<br>';
  247. }
  248. return messgage;
  249. },
  250. );
  251. this.showToasterByEmailList(failedEmailList, 'error');
  252. }
  253. } catch (err) {
  254. toastError(err);
  255. } finally {
  256. this.setState({ isCreateUserButtonPushed: false });
  257. }
  258. }
  259. handleInput(event) {
  260. this.setState({ emailInputValue: event.target.value });
  261. }
  262. handleCheckBox() {
  263. this.setState({ sendEmail: !this.state.sendEmail });
  264. }
  265. render() {
  266. const { t, adminUsersContainer } = this.props;
  267. const { invitedEmailList } = this.state;
  268. return (
  269. <Modal isOpen={adminUsersContainer.state.isUserInviteModalShown}>
  270. <ModalHeader tag="h4" toggle={this.onToggleModal} className="text-info">
  271. {t('admin:user_management.invite_users')}
  272. </ModalHeader>
  273. <ModalBody>
  274. {invitedEmailList == null
  275. ? this.renderModalBody()
  276. : this.renderCreatedModalBody()}
  277. </ModalBody>
  278. <ModalFooter className="d-flex">
  279. {invitedEmailList == null
  280. ? this.renderModalFooter()
  281. : this.renderCreatedModalFooter()}
  282. </ModalFooter>
  283. </Modal>
  284. );
  285. }
  286. }
  287. const UserInviteModalWrapperFC = (props) => {
  288. const { t } = useTranslation();
  289. const isMailerSetup = useAtomValue(isMailerSetupAtom);
  290. return (
  291. <UserInviteModal t={t} isMailerSetup={isMailerSetup ?? false} {...props} />
  292. );
  293. };
  294. /**
  295. * Wrapper component for using unstated
  296. */
  297. const UserInviteModalWrapper = withUnstatedContainers(
  298. UserInviteModalWrapperFC,
  299. [AdminUsersContainer],
  300. );
  301. UserInviteModal.propTypes = {
  302. t: PropTypes.func.isRequired, // i18next
  303. adminUsersContainer: PropTypes.instanceOf(AdminUsersContainer).isRequired,
  304. isMailerSetup: PropTypes.bool.isRequired,
  305. };
  306. export default UserInviteModalWrapper;