SearchPage.jsx 13 KB

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