UserManagement.tsx 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. import type React from 'react';
  2. import { useCallback, useEffect, useRef, useState } from 'react';
  3. import Link from 'next/link';
  4. import { useTranslation } from 'next-i18next';
  5. import AdminUsersContainer from '~/client/services/AdminUsersContainer';
  6. import { toastError } from '~/client/util/toastr';
  7. import PaginationWrapper from '../PaginationWrapper';
  8. import { withUnstatedContainers } from '../UnstatedUtils';
  9. import InviteUserControl from './Users/InviteUserControl';
  10. import PasswordResetModal from './Users/PasswordResetModal';
  11. import UserStatisticsTable from './Users/UserStatisticsTable';
  12. import UserTable from './Users/UserTable';
  13. import styles from './UserManagement.module.scss';
  14. type UserManagementProps = {
  15. adminUsersContainer: AdminUsersContainer;
  16. };
  17. const UserManagement = (props: UserManagementProps) => {
  18. const { t } = useTranslation('admin');
  19. const { adminUsersContainer } = props;
  20. const [isNotifyCommentShow, setIsNotifyCommentShow] = useState(false);
  21. const inputRef = useRef<HTMLInputElement>(null);
  22. const pagingHandler = useCallback(
  23. async (selectedPage: number) => {
  24. try {
  25. await adminUsersContainer.retrieveUsersByPagingNum(selectedPage);
  26. } catch (err) {
  27. toastError(err);
  28. }
  29. },
  30. [adminUsersContainer],
  31. );
  32. // for Next routing
  33. useEffect(() => {
  34. pagingHandler(1);
  35. adminUsersContainer.retrieveUserStatistics();
  36. }, [pagingHandler, adminUsersContainer]);
  37. const validateToggleStatus = (statusType: string) => {
  38. return adminUsersContainer.isSelected(statusType)
  39. ? adminUsersContainer.state.selectedStatusList.size > 1
  40. : true;
  41. };
  42. const clickHandler = (statusType: string) => {
  43. if (!validateToggleStatus(statusType)) {
  44. return setIsNotifyCommentShow(true);
  45. }
  46. if (isNotifyCommentShow) {
  47. setIsNotifyCommentShow(false);
  48. }
  49. adminUsersContainer.handleClick(statusType);
  50. };
  51. const resetButtonClickHandler = useCallback(async () => {
  52. try {
  53. await adminUsersContainer.resetAllChanges();
  54. setIsNotifyCommentShow(false);
  55. if (inputRef.current != null) {
  56. inputRef.current.value = '';
  57. }
  58. } catch (err) {
  59. toastError(err);
  60. }
  61. }, [adminUsersContainer]);
  62. const changeSearchTextHandler = useCallback(
  63. async (e: React.FormEvent<HTMLInputElement>) => {
  64. await adminUsersContainer.handleChangeSearchText(e?.currentTarget.value);
  65. },
  66. [adminUsersContainer],
  67. );
  68. const renderCheckbox = (
  69. status: string,
  70. statusLabel: string,
  71. statusColor: string,
  72. ) => {
  73. return (
  74. <div className={`form-check form-check-${statusColor} me-2`}>
  75. <input
  76. className="form-check-input"
  77. type="checkbox"
  78. id={`c_${status}`}
  79. checked={adminUsersContainer.isSelected(status)}
  80. onChange={() => clickHandler(status)}
  81. />
  82. <label className="form-label form-check-label" htmlFor={`c_${status}`}>
  83. <span
  84. className={`badge text-bg-${statusColor} d-inline-block vt mt-1`}
  85. >
  86. {statusLabel}
  87. </span>
  88. </label>
  89. </div>
  90. );
  91. };
  92. const pager = (
  93. <div className="my-3">
  94. <PaginationWrapper
  95. activePage={adminUsersContainer.state.activePage}
  96. changePage={pagingHandler}
  97. totalItemsCount={adminUsersContainer.state.totalUsers}
  98. pagingLimit={adminUsersContainer.state.pagingLimit}
  99. align="center"
  100. size="sm"
  101. />
  102. </div>
  103. );
  104. return (
  105. <div data-testid="admin-users">
  106. {adminUsersContainer.state.userForPasswordResetModal != null && (
  107. <PasswordResetModal
  108. isOpen={adminUsersContainer.state.isPasswordResetModalShown}
  109. onClose={adminUsersContainer.hidePasswordResetModal}
  110. userForPasswordResetModal={
  111. adminUsersContainer.state.userForPasswordResetModal
  112. }
  113. />
  114. )}
  115. <p>
  116. <InviteUserControl />
  117. <Link
  118. href="/admin/users/external-accounts"
  119. className="btn btn-outline-secondary ms-2"
  120. role="button"
  121. >
  122. <span className="material-symbols-outlined" aria-hidden="true">
  123. person_add
  124. </span>
  125. {t('admin:user_management.external_account')}
  126. </Link>
  127. </p>
  128. <h2>{t('user_management.user_management')}</h2>
  129. <UserStatisticsTable
  130. userStatistics={adminUsersContainer.state.userStatistics}
  131. />
  132. <div className="border-top border-bottom">
  133. <div className="row d-flex justify-content-start align-items-center my-2">
  134. <div className="col-md-3 d-flex align-items-center my-2">
  135. <span className="material-symbols-outlined">search</span>
  136. <span className={`search-typeahead ${styles['search-typeahead']}`}>
  137. <input
  138. className="w-100"
  139. type="text"
  140. ref={inputRef}
  141. onChange={changeSearchTextHandler}
  142. />
  143. {adminUsersContainer.state.searchText.length > 0 ? (
  144. <button
  145. type="button"
  146. className="btn btn-link p-0 material-symbols-outlined me-1 search-clear"
  147. aria-label={t('commons:Clear')}
  148. onClick={async () => {
  149. await adminUsersContainer.clearSearchText();
  150. if (inputRef.current != null) {
  151. inputRef.current.value = '';
  152. }
  153. }}
  154. >
  155. cancel
  156. </button>
  157. ) : (
  158. ''
  159. )}
  160. </span>
  161. </div>
  162. <div className="offset-md-1 col-md-6 my-2">
  163. <div>
  164. {renderCheckbox('all', 'All', 'primary')}
  165. {renderCheckbox('registered', 'Approval Pending', 'info')}
  166. {renderCheckbox('active', 'Active', 'success')}
  167. {renderCheckbox('suspended', 'Suspended', 'warning')}
  168. {renderCheckbox('invited', 'Invited', 'secondary')}
  169. </div>
  170. <div>
  171. {isNotifyCommentShow && (
  172. <span className="text-warning">
  173. {t('admin:user_management.click_twice_same_checkbox')}
  174. </span>
  175. )}
  176. </div>
  177. </div>
  178. <div className="col-md-2 my-2">
  179. <button
  180. type="button"
  181. className="btn btn-outline-secondary btn-sm"
  182. onClick={resetButtonClickHandler}
  183. >
  184. <span className="material-symbols-outlined">refresh</span>
  185. {t('commons:Reset')}
  186. </button>
  187. </div>
  188. </div>
  189. </div>
  190. {pager}
  191. <UserTable />
  192. {pager}
  193. </div>
  194. );
  195. };
  196. const UserManagementWrapper = withUnstatedContainers(UserManagement, [
  197. AdminUsersContainer,
  198. ]);
  199. export default UserManagementWrapper;