SearchPage.jsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  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, SORT_AXIS, SORT_ORDER } from '~/interfaces/search';
  13. import PageDeleteModal from './PageDeleteModal';
  14. export const specificPathNames = {
  15. user: '/user',
  16. trash: '/trash',
  17. };
  18. class SearchPage extends React.Component {
  19. constructor(props) {
  20. super(props);
  21. // NOTE : selectedPages is deletion related state, will be used later in story 77535, 77565.
  22. // deletionModal, deletion related functions are all removed, add them back when necessary.
  23. // i.e ) in story 77525 or any tasks implementing deletion functionalities
  24. this.state = {
  25. searchingKeyword: decodeURI(this.props.query.q) || '',
  26. searchedKeyword: '',
  27. searchResults: [],
  28. searchResultMeta: {},
  29. focusedSearchResultData: null,
  30. selectedPagesIdList: new Set(),
  31. searchResultCount: 0,
  32. activePage: 1,
  33. pagingLimit: this.props.appContainer.config.pageLimitationL,
  34. excludeUserPages: true,
  35. excludeTrashPages: true,
  36. sort: SORT_AXIS.RELATION_SCORE,
  37. order: SORT_ORDER.DESC,
  38. selectAllCheckboxType: CheckboxType.NONE_CHECKED,
  39. isDeleteConfirmModalShown: false,
  40. deleteTargetPageIds: new Set(),
  41. };
  42. this.changeURL = this.changeURL.bind(this);
  43. this.search = this.search.bind(this);
  44. this.onSearchInvoked = this.onSearchInvoked.bind(this);
  45. this.selectPage = this.selectPage.bind(this);
  46. this.toggleCheckBox = this.toggleCheckBox.bind(this);
  47. this.switchExcludeUserPagesHandler = this.switchExcludeUserPagesHandler.bind(this);
  48. this.switchExcludeTrashPagesHandler = this.switchExcludeTrashPagesHandler.bind(this);
  49. this.onChangeSortInvoked = this.onChangeSortInvoked.bind(this);
  50. this.onPagingNumberChanged = this.onPagingNumberChanged.bind(this);
  51. this.onPagingLimitChanged = this.onPagingLimitChanged.bind(this);
  52. this.deleteSinglePageButtonHandler = this.deleteSinglePageButtonHandler.bind(this);
  53. this.deleteAllPagesButtonHandler = this.deleteAllPagesButtonHandler.bind(this);
  54. this.closeDeleteConfirmModalHandler = this.closeDeleteConfirmModalHandler.bind(this);
  55. }
  56. componentDidMount() {
  57. const keyword = this.state.searchingKeyword;
  58. if (keyword !== '') {
  59. this.search({ keyword });
  60. }
  61. }
  62. static getQueryByLocation(location) {
  63. const search = location.search || '';
  64. const query = {};
  65. search.replace(/^\?/, '').split('&').forEach((element) => {
  66. const queryParts = element.split('=');
  67. query[queryParts[0]] = decodeURIComponent(queryParts[1]).replace(/\+/g, ' ');
  68. });
  69. return query;
  70. }
  71. switchExcludeUserPagesHandler() {
  72. this.setState({ excludeUserPages: !this.state.excludeUserPages });
  73. }
  74. switchExcludeTrashPagesHandler() {
  75. this.setState({ excludeTrashPages: !this.state.excludeTrashPages });
  76. }
  77. onChangeSortInvoked(nextSort, nextOrder) {
  78. this.setState({
  79. sort: nextSort,
  80. order: nextOrder,
  81. });
  82. }
  83. changeURL(keyword, refreshHash) {
  84. let hash = window.location.hash || '';
  85. // TODO 整理する
  86. if (refreshHash || this.state.searchedKeyword !== '') {
  87. hash = '';
  88. }
  89. if (window.history && window.history.pushState) {
  90. window.history.pushState('', `Search - ${keyword}`, `/_search?q=${keyword}${hash}`);
  91. }
  92. }
  93. createSearchQuery(keyword) {
  94. let query = keyword;
  95. // pages included in specific path are not retrived when prefix is added
  96. if (this.state.excludeTrashPages) {
  97. query = `${query} -prefix:${specificPathNames.trash}`;
  98. }
  99. if (this.state.excludeUserPages) {
  100. query = `${query} -prefix:${specificPathNames.user}`;
  101. }
  102. return query;
  103. }
  104. /**
  105. * this method is called when user changes paging number
  106. */
  107. async onPagingNumberChanged(activePage) {
  108. this.setState({ activePage }, () => this.search({ keyword: this.state.searchedKeyword }));
  109. }
  110. /**
  111. * this method is called when user searches by pressing Enter or using searchbox
  112. */
  113. async onSearchInvoked(data) {
  114. this.setState({ activePage: 1 }, () => this.search(data));
  115. }
  116. /**
  117. * change number of pages to display per page and execute search method after.
  118. */
  119. async onPagingLimitChanged(limit) {
  120. this.setState({ pagingLimit: limit }, () => this.search({ keyword: this.state.searchedKeyword }));
  121. }
  122. // todo: refactoring
  123. // refs: https://redmine.weseek.co.jp/issues/82139
  124. async search(data) {
  125. const keyword = data.keyword;
  126. if (keyword === '') {
  127. this.setState({
  128. searchingKeyword: '',
  129. searchedKeyword: '',
  130. searchResults: [],
  131. searchResultMeta: {},
  132. searchResultCount: 0,
  133. activePage: 1,
  134. });
  135. return true;
  136. }
  137. this.setState({
  138. searchingKeyword: keyword,
  139. });
  140. const pagingLimit = this.state.pagingLimit;
  141. const offset = (this.state.activePage * pagingLimit) - pagingLimit;
  142. const { sort, order } = this.state;
  143. try {
  144. const res = await this.props.appContainer.apiGet('/search', {
  145. q: this.createSearchQuery(keyword),
  146. limit: pagingLimit,
  147. offset,
  148. sort,
  149. order,
  150. });
  151. this.changeURL(keyword);
  152. if (res.data.length > 0) {
  153. this.setState({
  154. searchedKeyword: keyword,
  155. searchResults: res.data,
  156. searchResultMeta: res.meta,
  157. searchResultCount: res.meta.total,
  158. focusedSearchResultData: res.data[0],
  159. // reset active page if keyword changes, otherwise set the current state
  160. activePage: this.state.searchedKeyword === keyword ? this.state.activePage : 1,
  161. });
  162. }
  163. else {
  164. this.setState({
  165. searchedKeyword: keyword,
  166. searchResults: [],
  167. searchResultMeta: {},
  168. searchResultCount: 0,
  169. focusedSearchResultData: {},
  170. activePage: 1,
  171. });
  172. }
  173. }
  174. catch (err) {
  175. toastError(err);
  176. }
  177. }
  178. selectPage= (pageId) => {
  179. const index = this.state.searchResults.findIndex(({ pageData }) => {
  180. return pageData._id === pageId;
  181. });
  182. this.setState({
  183. focusedSearchResultData: this.state.searchResults[index],
  184. });
  185. }
  186. toggleCheckBox = (pageId) => {
  187. const { selectedPagesIdList } = this.state;
  188. if (selectedPagesIdList.has(pageId)) {
  189. selectedPagesIdList.delete(pageId);
  190. }
  191. else {
  192. selectedPagesIdList.add(pageId);
  193. }
  194. switch (selectedPagesIdList.size) {
  195. case 0:
  196. return this.setState({ selectAllCheckboxType: CheckboxType.NONE_CHECKED });
  197. case this.state.searchResults.length:
  198. return this.setState({ selectAllCheckboxType: CheckboxType.ALL_CHECKED });
  199. default:
  200. return this.setState({ selectAllCheckboxType: CheckboxType.INDETERMINATE });
  201. }
  202. }
  203. toggleAllCheckBox = (nextSelectAllCheckboxType) => {
  204. const { selectedPagesIdList, searchResults } = this.state;
  205. if (nextSelectAllCheckboxType === CheckboxType.NONE_CHECKED) {
  206. selectedPagesIdList.clear();
  207. }
  208. else {
  209. searchResults.forEach((page) => {
  210. selectedPagesIdList.add(page.pageData._id);
  211. });
  212. }
  213. this.setState({
  214. selectedPagesIdList,
  215. selectAllCheckboxType: nextSelectAllCheckboxType,
  216. });
  217. };
  218. getSelectedPagesToDelete() {
  219. const filteredPages = this.state.searchResults.filter((page) => {
  220. return Array.from(this.state.deleteTargetPageIds).find(id => id === page.pageData._id);
  221. });
  222. return filteredPages.map(page => ({
  223. pageId: page.pageData._id,
  224. revisionId: page.pageData.revision,
  225. path: page.pageData.path,
  226. }));
  227. }
  228. deleteSinglePageButtonHandler(pageId) {
  229. this.setState({ deleteTargetPageIds: new Set([pageId]) });
  230. this.setState({ isDeleteConfirmModalShown: true });
  231. }
  232. deleteAllPagesButtonHandler() {
  233. if (this.state.selectedPagesIdList.size === 0) { return }
  234. this.setState({ deleteTargetPageIds: this.state.selectedPagesIdList });
  235. this.setState({ isDeleteConfirmModalShown: true });
  236. }
  237. closeDeleteConfirmModalHandler() {
  238. this.setState({ isDeleteConfirmModalShown: false });
  239. }
  240. renderSearchResultContent = () => {
  241. return (
  242. <SearchResultContent
  243. appContainer={this.props.appContainer}
  244. searchingKeyword={this.state.searchingKeyword}
  245. focusedSearchResultData={this.state.focusedSearchResultData}
  246. >
  247. </SearchResultContent>
  248. );
  249. }
  250. renderSearchResultList = () => {
  251. return (
  252. <SearchResultList
  253. pages={this.state.searchResults || []}
  254. focusedSearchResultData={this.state.focusedSearchResultData}
  255. selectedPagesIdList={this.state.selectedPagesIdList || []}
  256. searchResultCount={this.state.searchResultCount}
  257. activePage={this.state.activePage}
  258. pagingLimit={this.state.pagingLimit}
  259. onClickSearchResultItem={this.selectPage}
  260. onClickCheckbox={this.toggleCheckBox}
  261. onPagingNumberChanged={this.onPagingNumberChanged}
  262. onClickDeleteButton={this.deleteSinglePageButtonHandler}
  263. />
  264. );
  265. }
  266. renderSearchControl = () => {
  267. return (
  268. <SearchControl
  269. searchingKeyword={this.state.searchingKeyword}
  270. sort={this.state.sort}
  271. order={this.state.order}
  272. searchResultCount={this.state.searchResultCount || 0}
  273. appContainer={this.props.appContainer}
  274. onSearchInvoked={this.onSearchInvoked}
  275. onClickSelectAllCheckbox={this.toggleAllCheckBox}
  276. selectAllCheckboxType={this.state.selectAllCheckboxType}
  277. onClickDeleteAllButton={this.deleteAllPagesButtonHandler}
  278. onExcludeUserPagesSwitched={this.switchExcludeUserPagesHandler}
  279. onExcludeTrashPagesSwitched={this.switchExcludeTrashPagesHandler}
  280. excludeUserPages={this.state.excludeUserPages}
  281. excludeTrashPages={this.state.excludeTrashPages}
  282. onChangeSortInvoked={this.onChangeSortInvoked}
  283. >
  284. </SearchControl>
  285. );
  286. }
  287. render() {
  288. return (
  289. <div>
  290. <SearchPageLayout
  291. SearchControl={this.renderSearchControl}
  292. SearchResultList={this.renderSearchResultList}
  293. SearchResultContent={this.renderSearchResultContent}
  294. searchResultMeta={this.state.searchResultMeta}
  295. searchingKeyword={this.state.searchedKeyword}
  296. onPagingLimitChanged={this.onPagingLimitChanged}
  297. initialPagingLimit={this.props.appContainer.config.pageLimitationL || 50}
  298. >
  299. </SearchPageLayout>
  300. <PageDeleteModal
  301. isOpen={this.state.isDeleteConfirmModalShown}
  302. onClose={this.closeDeleteConfirmModalHandler}
  303. pages={this.getSelectedPagesToDelete()}
  304. />
  305. </div>
  306. );
  307. }
  308. }
  309. /**
  310. * Wrapper component for using unstated
  311. */
  312. const SearchPageWrapper = withUnstatedContainers(SearchPage, [AppContainer]);
  313. SearchPage.propTypes = {
  314. t: PropTypes.func.isRequired, // i18next
  315. appContainer: PropTypes.instanceOf(AppContainer).isRequired,
  316. query: PropTypes.object,
  317. };
  318. SearchPage.defaultProps = {
  319. // pollInterval: 1000,
  320. query: SearchPage.getQueryByLocation(window.location || {}),
  321. };
  322. export default withTranslation()(SearchPageWrapper);