SearchPage.jsx 7.9 KB

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