SearchPage.jsx 11 KB

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