SearchPage.jsx 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. // This is the root component for #search-page
  2. import React from 'react';
  3. import PropTypes from 'prop-types';
  4. import { withTranslation } from 'react-i18next';
  5. import { withUnstatedContainers } from './UnstatedUtils';
  6. import AppContainer from '~/client/services/AppContainer';
  7. import { toastError } from '~/client/util/apiNotification';
  8. import SearchPageLayout from './SearchPage/SearchPageLayout';
  9. import SearchResultContent from './SearchPage/SearchResultContent';
  10. import SearchResultList from './SearchPage/SearchResultList';
  11. import SearchControl from './SearchPage/SearchControl';
  12. import { CheckboxType } from '../interfaces/search';
  13. export const specificPathNames = {
  14. user: '/user',
  15. trash: '/trash',
  16. };
  17. class SearchPage extends React.Component {
  18. constructor(props) {
  19. super(props);
  20. // NOTE : selectedPages is deletion related state, will be used later in story 77535, 77565.
  21. // deletionModal, deletion related functions are all removed, add them back when necessary.
  22. // i.e ) in story 77525 or any tasks implementing deletion functionalities
  23. this.state = {
  24. searchingKeyword: decodeURI(this.props.query.q) || '',
  25. searchedKeyword: '',
  26. searchedPages: [],
  27. searchResultMeta: {},
  28. focusedPage: {},
  29. selectedPagesIdList: new Set(),
  30. searchResultCount: 0,
  31. activePage: 1,
  32. pagingLimit: 10, // change to an appropriate limit number
  33. excludeUsersHome: true,
  34. excludeTrash: true,
  35. selectAllCheckboxType: CheckboxType.NONE_CHECKED,
  36. };
  37. this.changeURL = this.changeURL.bind(this);
  38. this.search = this.search.bind(this);
  39. this.searchHandler = this.searchHandler.bind(this);
  40. this.selectPage = this.selectPage.bind(this);
  41. this.toggleCheckBox = this.toggleCheckBox.bind(this);
  42. this.onExcludeUsersHome = this.onExcludeUsersHome.bind(this);
  43. this.onExcludeTrash = this.onExcludeTrash.bind(this);
  44. this.onPagingNumberChanged = this.onPagingNumberChanged.bind(this);
  45. }
  46. componentDidMount() {
  47. const keyword = this.state.searchingKeyword;
  48. if (keyword !== '') {
  49. this.search({ keyword });
  50. }
  51. }
  52. static getQueryByLocation(location) {
  53. const search = location.search || '';
  54. const query = {};
  55. search.replace(/^\?/, '').split('&').forEach((element) => {
  56. const queryParts = element.split('=');
  57. query[queryParts[0]] = decodeURIComponent(queryParts[1]).replace(/\+/g, ' ');
  58. });
  59. return query;
  60. }
  61. onExcludeUsersHome() {
  62. this.setState({ excludeUsersHome: !this.state.excludeUsersHome });
  63. }
  64. onExcludeTrash() {
  65. this.setState({ excludeTrash: !this.state.excludeTrash });
  66. }
  67. changeURL(keyword, refreshHash) {
  68. let hash = window.location.hash || '';
  69. // TODO 整理する
  70. if (refreshHash || this.state.searchedKeyword !== '') {
  71. hash = '';
  72. }
  73. if (window.history && window.history.pushState) {
  74. window.history.pushState('', `Search - ${keyword}`, `/_search?q=${keyword}${hash}`);
  75. }
  76. }
  77. createSearchQuery(keyword) {
  78. let query = keyword;
  79. // pages included in specific path are not retrived when prefix is added
  80. if (this.state.excludeTrash) {
  81. query = `${query} -prefix:${specificPathNames.trash}`;
  82. }
  83. if (this.state.excludeUsersHome) {
  84. query = `${query} -prefix:${specificPathNames.user}`;
  85. }
  86. return query;
  87. }
  88. /**
  89. * this method is called when user changes paging number
  90. */
  91. async onPagingNumberChanged(activePage) {
  92. // this.setState does not change the state immediately and following calls of this.search outside of this.setState will have old activePage state.
  93. // To prevent above, pass this.search as a callback function to make sure this.search will have the latest activePage state.
  94. this.setState({ activePage }, () => this.search({ keyword: this.state.searchedKeyword }));
  95. }
  96. /**
  97. * this method is called when user searches by pressing Enter or using searchbox
  98. */
  99. async searchHandler(data) {
  100. // this.setState does not change the state immediately and following calls of this.search outside of this.setState will have old activePage state.
  101. // To prevent above, pass this.search as a callback function to make sure this.search will have the latest activePage state.
  102. this.setState({ activePage: 1 }, () => this.search(data));
  103. }
  104. async search(data) {
  105. const keyword = data.keyword;
  106. if (keyword === '') {
  107. this.setState({
  108. searchingKeyword: '',
  109. searchedKeyword: '',
  110. searchedPages: [],
  111. searchResultMeta: {},
  112. searchResultCount: 0,
  113. activePage: 1,
  114. });
  115. return true;
  116. }
  117. this.setState({
  118. searchingKeyword: keyword,
  119. });
  120. const pagingLimit = this.state.pagingLimit;
  121. const offset = (this.state.activePage * pagingLimit) - pagingLimit;
  122. try {
  123. const res = await this.props.appContainer.apiGet('/search', {
  124. q: this.createSearchQuery(keyword),
  125. limit: pagingLimit,
  126. offset,
  127. });
  128. this.changeURL(keyword);
  129. if (res.data.length > 0) {
  130. this.setState({
  131. searchedKeyword: keyword,
  132. searchedPages: res.data,
  133. searchResultMeta: res.meta,
  134. searchResultCount: res.meta.total,
  135. focusedPage: res.data[0],
  136. // reset active page if keyword changes, otherwise set the current state
  137. activePage: this.state.searchedKeyword === keyword ? this.state.activePage : 1,
  138. });
  139. }
  140. else {
  141. this.setState({
  142. searchedKeyword: keyword,
  143. searchedPages: [],
  144. searchResultMeta: {},
  145. searchResultCount: 0,
  146. focusedPage: {},
  147. activePage: 1,
  148. });
  149. }
  150. }
  151. catch (err) {
  152. toastError(err);
  153. }
  154. }
  155. selectPage= (pageId) => {
  156. const index = this.state.searchedPages.findIndex((page) => {
  157. return page._id === pageId;
  158. });
  159. this.setState({
  160. focusedPage: this.state.searchedPages[index],
  161. });
  162. }
  163. toggleCheckBox = (pageId) => {
  164. const { selectedPagesIdList } = this.state;
  165. if (selectedPagesIdList.has(pageId)) {
  166. selectedPagesIdList.delete(pageId);
  167. }
  168. else {
  169. selectedPagesIdList.add(pageId);
  170. }
  171. switch (selectedPagesIdList.size) {
  172. case 0:
  173. return this.setState({ selectAllCheckboxType: CheckboxType.NONE_CHECKED });
  174. case this.state.searchedPages.length:
  175. return this.setState({ selectAllCheckboxType: CheckboxType.ALL_CHECKED });
  176. default:
  177. return this.setState({ selectAllCheckboxType: CheckboxType.INDETERMINATE });
  178. }
  179. }
  180. toggleAllCheckBox = (nextSelectAllCheckboxType) => {
  181. const { selectedPagesIdList, searchedPages } = this.state;
  182. if (nextSelectAllCheckboxType === CheckboxType.NONE_CHECKED) {
  183. selectedPagesIdList.clear();
  184. }
  185. else {
  186. searchedPages.forEach((page) => {
  187. selectedPagesIdList.add(page._id);
  188. });
  189. }
  190. this.setState({
  191. selectedPagesIdList,
  192. selectAllCheckboxType: nextSelectAllCheckboxType,
  193. });
  194. };
  195. renderSearchResultContent = () => {
  196. return (
  197. <SearchResultContent
  198. appContainer={this.props.appContainer}
  199. searchingKeyword={this.state.searchingKeyword}
  200. focusedPage={this.state.focusedPage}
  201. >
  202. </SearchResultContent>
  203. );
  204. }
  205. renderSearchResultList = () => {
  206. return (
  207. <SearchResultList
  208. pages={this.state.searchedPages || []}
  209. focusedPage={this.state.focusedPage}
  210. selectedPagesIdList={this.state.selectedPagesIdList || []}
  211. searchResultCount={this.state.searchResultCount}
  212. activePage={this.state.activePage}
  213. pagingLimit={this.state.pagingLimit}
  214. onClickSearchResultItem={this.selectPage}
  215. onClickCheckbox={this.toggleCheckBox}
  216. onPagingNumberChanged={this.onPagingNumberChanged}
  217. />
  218. );
  219. }
  220. renderSearchControl = () => {
  221. return (
  222. <SearchControl
  223. searchingKeyword={this.state.searchingKeyword}
  224. appContainer={this.props.appContainer}
  225. onSearchInvoked={this.searchHandler}
  226. onExcludeUsersHome={this.onExcludeUsersHome}
  227. onExcludeTrash={this.onExcludeTrash}
  228. onClickSelectAllCheckbox={this.toggleAllCheckBox}
  229. selectAllCheckboxType={this.state.selectAllCheckboxType}
  230. >
  231. </SearchControl>
  232. );
  233. }
  234. render() {
  235. return (
  236. <div>
  237. <SearchPageLayout
  238. SearchControl={this.renderSearchControl}
  239. SearchResultList={this.renderSearchResultList}
  240. SearchResultContent={this.renderSearchResultContent}
  241. searchResultMeta={this.state.searchResultMeta}
  242. searchingKeyword={this.state.searchedKeyword}
  243. >
  244. </SearchPageLayout>
  245. </div>
  246. );
  247. }
  248. }
  249. /**
  250. * Wrapper component for using unstated
  251. */
  252. const SearchPageWrapper = withUnstatedContainers(SearchPage, [AppContainer]);
  253. SearchPage.propTypes = {
  254. t: PropTypes.func.isRequired, // i18next
  255. appContainer: PropTypes.instanceOf(AppContainer).isRequired,
  256. query: PropTypes.object,
  257. };
  258. SearchPage.defaultProps = {
  259. // pollInterval: 1000,
  260. query: SearchPage.getQueryByLocation(window.location || {}),
  261. };
  262. export default withTranslation()(SearchPageWrapper);