SearchControl.tsx 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. import { useTranslation } from 'next-i18next';
  2. import React, { type JSX, useCallback, useEffect, useState } from 'react';
  3. import { Collapse } from 'reactstrap';
  4. import { SORT_AXIS, SORT_ORDER } from '~/interfaces/search';
  5. import type { ISearchConditions, ISearchConfigurations } from '~/stores/search';
  6. import styles from './SearchControl.module.scss';
  7. import { SearchModalTriggerinput } from './SearchModalTriggerinput';
  8. import SearchOptionModal from './SearchOptionModal';
  9. import SortControl from './SortControl';
  10. type Props = {
  11. isEnableSort: boolean;
  12. isEnableFilter: boolean;
  13. initialSearchConditions: Partial<ISearchConditions>;
  14. onSearchInvoked?: (
  15. keyword: string,
  16. configurations: Partial<ISearchConfigurations>,
  17. ) => void;
  18. extraControls: React.ReactNode;
  19. collapseContents?: React.ReactNode;
  20. isCollapsed?: boolean;
  21. };
  22. const SearchControl = React.memo((props: Props): JSX.Element => {
  23. const {
  24. isEnableSort,
  25. isEnableFilter,
  26. initialSearchConditions,
  27. onSearchInvoked,
  28. extraControls,
  29. collapseContents,
  30. isCollapsed,
  31. } = props;
  32. const keywordOnInit = initialSearchConditions.keyword ?? '';
  33. const [keyword, setKeyword] = useState(keywordOnInit);
  34. const [sort, setSort] = useState<SORT_AXIS>(
  35. initialSearchConditions.sort ?? SORT_AXIS.RELATION_SCORE,
  36. );
  37. const [order, setOrder] = useState<SORT_ORDER>(
  38. initialSearchConditions.order ?? SORT_ORDER.DESC,
  39. );
  40. const [includeUserPages, setIncludeUserPages] = useState(
  41. initialSearchConditions.includeUserPages ?? false,
  42. );
  43. const [includeTrashPages, setIncludeTrashPages] = useState(
  44. initialSearchConditions.includeTrashPages ?? false,
  45. );
  46. const [isFileterOptionModalShown, setIsFileterOptionModalShown] =
  47. useState(false);
  48. const { t } = useTranslation('');
  49. const searchBySearchControlHandler = useCallback(
  50. (newKeyword: string) => {
  51. setKeyword(newKeyword);
  52. onSearchInvoked?.(newKeyword, {
  53. sort,
  54. order,
  55. includeUserPages,
  56. includeTrashPages,
  57. });
  58. },
  59. [includeTrashPages, includeUserPages, onSearchInvoked, order, sort],
  60. );
  61. const changeSortHandler = useCallback(
  62. (nextSort: SORT_AXIS, nextOrder: SORT_ORDER) => {
  63. setSort(nextSort);
  64. setOrder(nextOrder);
  65. onSearchInvoked?.(keyword, {
  66. sort: nextSort,
  67. order: nextOrder,
  68. includeUserPages,
  69. includeTrashPages,
  70. });
  71. },
  72. [includeTrashPages, includeUserPages, keyword, onSearchInvoked],
  73. );
  74. const changeIncludeUserPagesHandler = useCallback(
  75. (include: boolean) => {
  76. setIncludeUserPages(include);
  77. onSearchInvoked?.(keyword, {
  78. sort,
  79. order,
  80. includeUserPages: include,
  81. includeTrashPages,
  82. });
  83. },
  84. [includeTrashPages, keyword, onSearchInvoked, order, sort],
  85. );
  86. const changeIncludeTrashPagesHandler = useCallback(
  87. (include: boolean) => {
  88. setIncludeTrashPages(include);
  89. onSearchInvoked?.(keyword, {
  90. sort,
  91. order,
  92. includeUserPages,
  93. includeTrashPages: include,
  94. });
  95. },
  96. [includeUserPages, keyword, onSearchInvoked, order, sort],
  97. );
  98. useEffect(() => {
  99. setKeyword(keywordOnInit);
  100. }, [keywordOnInit]);
  101. return (
  102. <div className="shadow-sm">
  103. <div className="grw-search-page-nav d-flex py-3 align-items-center">
  104. <div className="flex-grow-1 mx-4">
  105. <SearchModalTriggerinput
  106. keywordOnInit={keyword}
  107. onSearchInvoked={searchBySearchControlHandler}
  108. />
  109. </div>
  110. </div>
  111. {/* TODO: replace the following elements deleteAll button , relevance button and include specificPath button component */}
  112. <div className="search-control d-flex align-items-center py-md-2 py-3 px-md-4 px-3 border-bottom border-gray">
  113. {/* sort option */}
  114. {isEnableSort && (
  115. <div className="flex-grow-1">
  116. <SortControl
  117. sort={sort}
  118. order={order}
  119. onChange={changeSortHandler}
  120. />
  121. </div>
  122. )}
  123. {/* filter option */}
  124. {isEnableFilter && (
  125. <>
  126. <div className="d-lg-none">
  127. <button
  128. type="button"
  129. className="btn"
  130. onClick={() => setIsFileterOptionModalShown(true)}
  131. >
  132. <span className="material-symbols-outlined">tune</span>
  133. </button>
  134. </div>
  135. <div className="d-none d-lg-flex align-items-center search-control-include-options">
  136. <div className="px-2 py-1">
  137. <div className="form-check form-check-succsess">
  138. <input
  139. className="form-check-input me-2"
  140. type="checkbox"
  141. id="flexCheckDefault"
  142. defaultChecked={includeUserPages}
  143. onChange={(e) =>
  144. changeIncludeUserPagesHandler(e.target.checked)
  145. }
  146. />
  147. <label
  148. className="form-label form-check-label mb-0 d-flex align-items-center text-secondary with-no-font-weight"
  149. htmlFor="flexCheckDefault"
  150. >
  151. {t('Include Subordinated Target Page', { target: '/user' })}
  152. </label>
  153. </div>
  154. </div>
  155. <div className="px-2 py-1">
  156. <div className="form-check form-check-succsess">
  157. <input
  158. className="form-check-input me-2"
  159. type="checkbox"
  160. id="flexCheckChecked"
  161. checked={includeTrashPages}
  162. onChange={(e) =>
  163. changeIncludeTrashPagesHandler(e.target.checked)
  164. }
  165. />
  166. <label
  167. className="form-label form-check-label mb-0 d-flex align-items-center text-secondary with-no-font-weight"
  168. htmlFor="flexCheckChecked"
  169. >
  170. {t('Include Subordinated Target Page', {
  171. target: '/trash',
  172. })}
  173. </label>
  174. </div>
  175. </div>
  176. </div>
  177. </>
  178. )}
  179. {extraControls}
  180. </div>
  181. {collapseContents != null && (
  182. <Collapse isOpen={isCollapsed}>{collapseContents}</Collapse>
  183. )}
  184. <SearchOptionModal
  185. isOpen={isFileterOptionModalShown || false}
  186. onClose={() => setIsFileterOptionModalShown(false)}
  187. includeUserPages={includeUserPages}
  188. includeTrashPages={includeTrashPages}
  189. onIncludeUserPagesSwitched={setIncludeUserPages}
  190. onIncludeTrashPagesSwitched={setIncludeTrashPages}
  191. />
  192. </div>
  193. );
  194. });
  195. SearchControl.displayName = 'SearchControl';
  196. export default SearchControl;