SearchPage.jsx 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  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. searchResults: [],
  26. searchResultMeta: {},
  27. focusedSearchResultData: null,
  28. selectedPages: new Set(),
  29. searchResultCount: 0,
  30. activePage: 1,
  31. pagingLimit: this.props.appContainer.config.pageLimitationL || 50,
  32. excludeUserPages: true,
  33. excludeTrashPages: true,
  34. };
  35. this.changeURL = this.changeURL.bind(this);
  36. this.search = this.search.bind(this);
  37. this.onSearchInvoked = this.onSearchInvoked.bind(this);
  38. this.selectPage = this.selectPage.bind(this);
  39. this.toggleCheckBox = this.toggleCheckBox.bind(this);
  40. this.switchExcludeUserPagesHandler = this.switchExcludeUserPagesHandler.bind(this);
  41. this.switchExcludeTrashPagesHandler = this.switchExcludeTrashPagesHandler.bind(this);
  42. this.onPagingNumberChanged = this.onPagingNumberChanged.bind(this);
  43. this.onPagingLimitChanged = this.onPagingLimitChanged.bind(this);
  44. }
  45. componentDidMount() {
  46. const keyword = this.state.searchingKeyword;
  47. if (keyword !== '') {
  48. this.search({ keyword });
  49. }
  50. }
  51. static getQueryByLocation(location) {
  52. const search = location.search || '';
  53. const query = {};
  54. search.replace(/^\?/, '').split('&').forEach((element) => {
  55. const queryParts = element.split('=');
  56. query[queryParts[0]] = decodeURIComponent(queryParts[1]).replace(/\+/g, ' ');
  57. });
  58. return query;
  59. }
  60. switchExcludeUserPagesHandler() {
  61. this.setState({ excludeUserPages: !this.state.excludeUserPages });
  62. }
  63. switchExcludeTrashPagesHandler() {
  64. this.setState({ excludeTrashPages: !this.state.excludeTrashPages });
  65. }
  66. changeURL(keyword, refreshHash) {
  67. let hash = window.location.hash || '';
  68. // TODO 整理する
  69. if (refreshHash || this.state.searchedKeyword !== '') {
  70. hash = '';
  71. }
  72. if (window.history && window.history.pushState) {
  73. window.history.pushState('', `Search - ${keyword}`, `/_search?q=${keyword}${hash}`);
  74. }
  75. }
  76. createSearchQuery(keyword) {
  77. let query = keyword;
  78. // pages included in specific path are not retrived when prefix is added
  79. if (this.state.excludeTrashPages) {
  80. query = `${query} -prefix:${specificPathNames.trash}`;
  81. }
  82. if (this.state.excludeUserPages) {
  83. query = `${query} -prefix:${specificPathNames.user}`;
  84. }
  85. return query;
  86. }
  87. /**
  88. * this method is called when user changes paging number
  89. */
  90. async onPagingNumberChanged(activePage) {
  91. this.setState({ activePage }, () => this.search({ keyword: this.state.searchedKeyword }));
  92. }
  93. /**
  94. * this method is called when user searches by pressing Enter or using searchbox
  95. */
  96. async onSearchInvoked(data) {
  97. this.setState({ activePage: 1 }, () => this.search(data));
  98. }
  99. /**
  100. * change number of pages to display per page and execute search method after.
  101. */
  102. async onPagingLimitChanged(limit) {
  103. this.setState({ pagingLimit: limit }, () => this.search({ keyword: this.state.searchedKeyword }));
  104. }
  105. // todo: refactoring
  106. // refs: https://redmine.weseek.co.jp/issues/82139
  107. async search(data) {
  108. const keyword = data.keyword;
  109. if (keyword === '') {
  110. this.setState({
  111. searchingKeyword: '',
  112. searchedKeyword: '',
  113. searchResults: [],
  114. searchResultMeta: {},
  115. searchResultCount: 0,
  116. activePage: 1,
  117. });
  118. return true;
  119. }
  120. this.setState({
  121. searchingKeyword: keyword,
  122. });
  123. const pagingLimit = this.state.pagingLimit;
  124. const offset = (this.state.activePage * pagingLimit) - pagingLimit;
  125. try {
  126. const res = await this.props.appContainer.apiGet('/search', {
  127. q: this.createSearchQuery(keyword),
  128. limit: pagingLimit,
  129. offset,
  130. });
  131. this.changeURL(keyword);
  132. if (res.data.length > 0) {
  133. this.setState({
  134. searchedKeyword: keyword,
  135. searchResults: res.data,
  136. searchResultMeta: res.meta,
  137. searchResultCount: res.meta.total,
  138. focusedSearchResultData: res.data[0],
  139. // reset active page if keyword changes, otherwise set the current state
  140. activePage: this.state.searchedKeyword === keyword ? this.state.activePage : 1,
  141. });
  142. }
  143. else {
  144. this.setState({
  145. searchedKeyword: keyword,
  146. searchResults: [],
  147. searchResultMeta: {},
  148. searchResultCount: 0,
  149. focusedSearchResultData: {},
  150. activePage: 1,
  151. });
  152. }
  153. }
  154. catch (err) {
  155. toastError(err);
  156. }
  157. }
  158. selectPage= (pageId) => {
  159. const index = this.state.searchResults.findIndex(({ pageData }) => {
  160. return pageData._id === pageId;
  161. });
  162. this.setState({
  163. focusedSearchResultData: this.state.searchResults[index],
  164. });
  165. }
  166. toggleCheckBox = (page) => {
  167. if (this.state.selectedPages.has(page)) {
  168. this.state.selectedPages.delete(page);
  169. }
  170. else {
  171. this.state.selectedPages.add(page);
  172. }
  173. }
  174. renderSearchResultContent = () => {
  175. return (
  176. <SearchResultContent
  177. appContainer={this.props.appContainer}
  178. searchingKeyword={this.state.searchingKeyword}
  179. focusedSearchResultData={this.state.focusedSearchResultData}
  180. >
  181. </SearchResultContent>
  182. );
  183. }
  184. renderSearchResultList = () => {
  185. return (
  186. <SearchResultList
  187. pages={this.state.searchResults || []}
  188. focusedSearchResultData={this.state.focusedSearchResultData}
  189. selectedPages={this.state.selectedPages || []}
  190. searchResultCount={this.state.searchResultCount}
  191. activePage={this.state.activePage}
  192. pagingLimit={this.state.pagingLimit}
  193. onClickInvoked={this.selectPage}
  194. onChangedInvoked={this.toggleCheckBox}
  195. onPagingNumberChanged={this.onPagingNumberChanged}
  196. />
  197. );
  198. }
  199. renderSearchControl = () => {
  200. return (
  201. <SearchControl
  202. searchingKeyword={this.state.searchingKeyword}
  203. appContainer={this.props.appContainer}
  204. onSearchInvoked={this.onSearchInvoked}
  205. onExcludeUserPagesSwitched={this.switchExcludeUserPagesHandler}
  206. onExcludeTrashPagesSwitched={this.switchExcludeTrashPagesHandler}
  207. excludeUserPages={this.state.excludeUserPages}
  208. excludeTrashPages={this.state.excludeTrashPages}
  209. >
  210. </SearchControl>
  211. );
  212. }
  213. render() {
  214. return (
  215. <div>
  216. <SearchPageLayout
  217. SearchControl={this.renderSearchControl}
  218. SearchResultList={this.renderSearchResultList}
  219. SearchResultContent={this.renderSearchResultContent}
  220. searchResultMeta={this.state.searchResultMeta}
  221. searchingKeyword={this.state.searchedKeyword}
  222. onPagingLimitChanged={this.onPagingLimitChanged}
  223. pagingLimit={this.state.pagingLimit}
  224. activePage={this.state.activePage}
  225. >
  226. </SearchPageLayout>
  227. </div>
  228. );
  229. }
  230. }
  231. /**
  232. * Wrapper component for using unstated
  233. */
  234. const SearchPageWrapper = withUnstatedContainers(SearchPage, [AppContainer]);
  235. SearchPage.propTypes = {
  236. t: PropTypes.func.isRequired, // i18next
  237. appContainer: PropTypes.instanceOf(AppContainer).isRequired,
  238. query: PropTypes.object,
  239. };
  240. SearchPage.defaultProps = {
  241. // pollInterval: 1000,
  242. query: SearchPage.getQueryByLocation(window.location || {}),
  243. };
  244. export default withTranslation()(SearchPageWrapper);