UserGroupUserFormByInput.tsx 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. import type { FC, KeyboardEvent } from 'react';
  2. import React, { useState, useRef } from 'react';
  3. import type { IUserGroupHasId, IUserHasId } from '@growi/core';
  4. import { UserPicture } from '@growi/ui/dist/components';
  5. import { useTranslation } from 'next-i18next';
  6. import { AsyncTypeahead } from 'react-bootstrap-typeahead';
  7. import { toastSuccess, toastError } from '~/client/util/toastr';
  8. import type { SearchType } from '~/interfaces/user-group';
  9. import Xss from '~/services/xss';
  10. type Props = {
  11. userGroup: IUserGroupHasId,
  12. onClickAddUserBtn: (username: string) => Promise<void>,
  13. onSearchApplicableUsers: (searchWord: string) => Promise<IUserHasId[]>,
  14. isAlsoNameSearched: boolean,
  15. isAlsoMailSearched: boolean,
  16. searchType: SearchType,
  17. }
  18. export const UserGroupUserFormByInput: FC<Props> = (props) => {
  19. const {
  20. userGroup, onClickAddUserBtn, onSearchApplicableUsers, isAlsoNameSearched, isAlsoMailSearched, searchType,
  21. } = props;
  22. const { t } = useTranslation();
  23. const typeaheadRef = useRef(null);
  24. const [inputUser, setInputUser] = useState<IUserHasId[]>([]);
  25. const [applicableUsers, setApplicableUsers] = useState<IUserHasId[]>([]);
  26. const [isLoading, setIsLoading] = useState(false);
  27. const [isSearchError, setIsSearchError] = useState(false);
  28. const xss = new Xss();
  29. const addUserBySubmit = async() => {
  30. if (inputUser.length === 0) { return }
  31. const userName = inputUser[0].username;
  32. try {
  33. await onClickAddUserBtn(userName);
  34. toastSuccess(`Added "${xss.process(userName)}" to "${xss.process(userGroup.name)}"`);
  35. setInputUser([]);
  36. }
  37. catch (err) {
  38. toastError(new Error(`Unable to add "${xss.process(userName)}" to "${xss.process(userGroup.name)}"`));
  39. }
  40. };
  41. const searchApplicableUsers = async(keyword: string) => {
  42. try {
  43. const users = await onSearchApplicableUsers(keyword);
  44. setApplicableUsers(users);
  45. setIsLoading(false);
  46. }
  47. catch (err) {
  48. setIsSearchError(true);
  49. toastError(err);
  50. }
  51. };
  52. const handleChange = (inputUser: IUserHasId[]) => {
  53. setInputUser(inputUser);
  54. };
  55. const handleSearch = async(keyword: string) => {
  56. setIsLoading(true);
  57. await searchApplicableUsers(keyword);
  58. };
  59. const onKeyDown = (event: KeyboardEvent) => {
  60. if (event.key === 'Enter') {
  61. addUserBySubmit();
  62. }
  63. };
  64. const renderMenuItemChildren = (option: IUserHasId) => {
  65. const user = option;
  66. return (
  67. <>
  68. <UserPicture user={user} size="sm" noLink noTooltip />
  69. <strong className="ms-2">{user.username}</strong>
  70. {isAlsoNameSearched && <span className="ms-2">{user.name}</span>}
  71. {isAlsoMailSearched && <span className="ms-2">{user.email}</span>}
  72. </>
  73. );
  74. };
  75. return (
  76. <div className="row">
  77. <div className="col-8 pe-0">
  78. <AsyncTypeahead
  79. key={`${searchType}-${isAlsoNameSearched}-${isAlsoMailSearched}`} // The searched keywords are not re-searched, so re-rendered by key.
  80. id="name-typeahead-asynctypeahead"
  81. inputProps={{ autoComplete: 'off' }}
  82. isLoading={isLoading}
  83. labelKey={(user: IUserHasId) => `${user.username} ${user.name} ${user.email}`}
  84. options={applicableUsers} // Search result
  85. onSearch={handleSearch}
  86. onChange={handleChange}
  87. onKeyDown={onKeyDown}
  88. minLength={1}
  89. searchText={isLoading ? 'Searching...' : (isSearchError && 'Error on searching.')}
  90. renderMenuItemChildren={renderMenuItemChildren}
  91. align="left"
  92. clearButton
  93. />
  94. </div>
  95. <div className="col-2 ps-0">
  96. <button
  97. type="button"
  98. className="btn btn-success"
  99. disabled={inputUser.length === 0}
  100. onClick={addUserBySubmit}
  101. >
  102. {t('add')}
  103. </button>
  104. </div>
  105. </div>
  106. );
  107. };