SearchResult.jsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. import React from 'react';
  2. import PropTypes from 'prop-types';
  3. import * as toastr from 'toastr';
  4. import { withTranslation } from 'react-i18next';
  5. import Page from '../PageList/Page';
  6. import SearchResultList from './SearchResultList';
  7. import DeletePageListModal from './DeletePageListModal';
  8. import AppContainer from '../../services/AppContainer';
  9. import { withUnstatedContainers } from '../UnstatedUtils';
  10. class SearchResult extends React.Component {
  11. constructor(props) {
  12. super(props);
  13. this.state = {
  14. deletionMode: false,
  15. selectedPages: new Set(),
  16. isDeleteCompletely: undefined,
  17. isDeleteConfirmModalShown: false,
  18. errorMessageForDeleting: undefined,
  19. };
  20. this.toggleDeleteCompletely = this.toggleDeleteCompletely.bind(this);
  21. this.deleteSelectedPages = this.deleteSelectedPages.bind(this);
  22. this.closeDeleteConfirmModal = this.closeDeleteConfirmModal.bind(this);
  23. }
  24. isNotSearchedYet() {
  25. return !this.props.searchResultMeta.took;
  26. }
  27. isNotFound() {
  28. return this.props.searchingKeyword !== '' && this.props.pages.length === 0;
  29. }
  30. isError() {
  31. if (this.props.searchError !== null) {
  32. return true;
  33. }
  34. return false;
  35. }
  36. /**
  37. * move the page
  38. */
  39. visitPageButtonHandler(e) {
  40. window.location.href = e.currentTarget.value;
  41. }
  42. /**
  43. * toggle checkbox and add (or delete from) selected pages list
  44. *
  45. * @param {any} page
  46. * @memberof SearchResult
  47. */
  48. toggleCheckbox(page) {
  49. if (this.state.selectedPages.has(page)) {
  50. this.state.selectedPages.delete(page);
  51. }
  52. else {
  53. this.state.selectedPages.add(page);
  54. }
  55. this.setState({ isDeleteConfirmModalShown: false });
  56. this.setState({ selectedPages: this.state.selectedPages });
  57. }
  58. /**
  59. * check and return is all pages selected for delete?
  60. *
  61. * @returns all pages selected (or not)
  62. * @memberof SearchResult
  63. */
  64. isAllSelected() {
  65. return this.state.selectedPages.size === this.props.pages.length;
  66. }
  67. /**
  68. * handle checkbox clicking that all pages select for delete
  69. *
  70. * @memberof SearchResult
  71. */
  72. handleAllSelect() {
  73. if (this.isAllSelected()) {
  74. this.state.selectedPages.clear();
  75. }
  76. else {
  77. this.state.selectedPages.clear();
  78. this.props.pages.map((page) => {
  79. this.state.selectedPages.add(page);
  80. return;
  81. });
  82. }
  83. this.setState({ selectedPages: this.state.selectedPages });
  84. }
  85. /**
  86. * change deletion mode
  87. *
  88. * @memberof SearchResult
  89. */
  90. handleDeletionModeChange() {
  91. this.state.selectedPages.clear();
  92. this.setState({ deletionMode: !this.state.deletionMode });
  93. }
  94. /**
  95. * toggle check delete completely
  96. *
  97. * @memberof SearchResult
  98. */
  99. toggleDeleteCompletely() {
  100. // request で completely が undefined でないと指定アリと見なされるため
  101. this.setState({ isDeleteCompletely: this.state.isDeleteCompletely ? undefined : true });
  102. }
  103. /**
  104. * delete selected pages
  105. *
  106. * @memberof SearchResult
  107. */
  108. deleteSelectedPages() {
  109. const deleteCompletely = this.state.isDeleteCompletely;
  110. Promise.all(Array.from(this.state.selectedPages).map((page) => {
  111. return new Promise((resolve, reject) => {
  112. const pageId = page._id;
  113. const revisionId = page.revision._id;
  114. this.props.appContainer.apiPost('/pages.remove', { page_id: pageId, revision_id: revisionId, completely: deleteCompletely })
  115. .then((res) => {
  116. if (res.ok) {
  117. this.state.selectedPages.delete(page);
  118. return resolve();
  119. }
  120. return reject();
  121. })
  122. .catch((err) => {
  123. console.log(err.message); // eslint-disable-line no-console
  124. this.setState({ errorMessageForDeleting: err.message });
  125. return reject();
  126. });
  127. });
  128. }))
  129. .then(() => {
  130. window.location.reload();
  131. })
  132. .catch((err) => {
  133. toastr.error(err, 'Error occured', {
  134. closeButton: true,
  135. progressBar: true,
  136. newestOnTop: false,
  137. showDuration: '100',
  138. hideDuration: '100',
  139. timeOut: '3000',
  140. });
  141. });
  142. }
  143. /**
  144. * open confirm modal for page selection delete
  145. *
  146. * @memberof SearchResult
  147. */
  148. showDeleteConfirmModal() {
  149. this.setState({ isDeleteConfirmModalShown: true });
  150. }
  151. /**
  152. * close confirm modal for page selection delete
  153. *
  154. * @memberof SearchResult
  155. */
  156. closeDeleteConfirmModal() {
  157. this.setState({
  158. isDeleteConfirmModalShown: false,
  159. errorMessageForDeleting: undefined,
  160. });
  161. }
  162. renderListView(pages) {
  163. return pages.map((page) => {
  164. // Add prefix 'id_' in pageId, because scrollspy of bootstrap doesn't work when the first letter of id attr of target component is numeral.
  165. const pageId = `#id_${page._id}`;
  166. return (
  167. <li key={page._id} className="nav-item page-list-li w-100">
  168. <a className="nav-link page-list-link d-flex align-items-center" href={pageId}>
  169. <Page page={page} noLink />
  170. <div className="ml-auto d-flex">
  171. { this.state.deletionMode
  172. && (
  173. <div className="custom-control custom-checkbox custom-checkbox-danger">
  174. <input
  175. type="checkbox"
  176. id={`page-delete-check-${page._id}`}
  177. className="custom-control-input search-result-list-delete-checkbox"
  178. value={pageId}
  179. checked={this.state.selectedPages.has(page)}
  180. onChange={() => { return this.toggleCheckbox(page) }}
  181. />
  182. <label className="custom-control-label" htmlFor={`page-delete-check-${page._id}`}></label>
  183. </div>
  184. )
  185. }
  186. <div className="page-list-option">
  187. <button type="button" className="btn btn-link p-0" value={page.path} onClick={this.visitPageButtonHandler}><i className="icon-login" /></button>
  188. </div>
  189. </div>
  190. </a>
  191. </li>
  192. );
  193. });
  194. }
  195. render() {
  196. const { t } = this.props;
  197. if (this.isError()) {
  198. return (
  199. <div className="content-main">
  200. <i className="searcing fa fa-warning"></i> Error on searching.
  201. </div>
  202. );
  203. }
  204. if (this.isNotSearchedYet()) {
  205. return <div />;
  206. }
  207. if (this.isNotFound()) {
  208. let under = '';
  209. if (this.props.tree != null) {
  210. under = ` under "${this.props.tree}"`;
  211. }
  212. return (
  213. <div className="content-main">
  214. <i className="icon-fw icon-info" /> No page found with &quot;{this.props.searchingKeyword}&quot;{under}
  215. </div>
  216. );
  217. }
  218. let deletionModeButtons = '';
  219. let allSelectCheck = '';
  220. if (this.state.deletionMode) {
  221. deletionModeButtons = (
  222. <div className="btn-group">
  223. <button type="button" className="btn btn-outline-secondary btn-sm rounded-pill-weak" onClick={() => { return this.handleDeletionModeChange() }}>
  224. <i className="icon-ban" /> {t('search_result.cancel')}
  225. </button>
  226. <button
  227. type="button"
  228. className="btn btn-danger btn-sm rounded-pill-weak"
  229. onClick={() => { return this.showDeleteConfirmModal() }}
  230. disabled={this.state.selectedPages.size === 0}
  231. >
  232. <i className="icon-trash" /> {t('search_result.delete')}
  233. </button>
  234. </div>
  235. );
  236. allSelectCheck = (
  237. <div className="custom-control custom-checkbox custom-checkbox-danger">
  238. <input
  239. id="all-select-check"
  240. className="custom-control-input"
  241. type="checkbox"
  242. onChange={() => { return this.handleAllSelect() }}
  243. checked={this.isAllSelected()}
  244. />
  245. <label className="custom-control-label" htmlFor="all-select-check">&nbsp;{t('search_result.check_all')}</label>
  246. </div>
  247. );
  248. }
  249. else {
  250. deletionModeButtons = (
  251. <div className="btn-group">
  252. <button type="button" className="btn btn-outline-secondary rounded-pill btn-sm" onClick={() => { return this.handleDeletionModeChange() }}>
  253. <i className="ti-check-box" /> {t('search_result.deletion_mode_btn_lavel')}
  254. </button>
  255. </div>
  256. );
  257. }
  258. const listView = this.renderListView(this.props.pages);
  259. /*
  260. UI あとで考える
  261. <span className="search-result-meta">Found: {this.props.searchResultMeta.total} pages with "{this.props.searchingKeyword}"</span>
  262. */
  263. return (
  264. <div className="content-main">
  265. <div className="search-result row" id="search-result">
  266. <div className="col-lg-4 d-none d-lg-block page-list search-result-list pr-0" id="search-result-list">
  267. <nav>
  268. <div className="d-flex align-items-start justify-content-between mt-1">
  269. <div className="search-result-meta">
  270. <i className="icon-magnifier" /> Found {this.props.searchResultMeta.total} pages with &quot;{this.props.searchingKeyword}&quot;
  271. </div>
  272. <div className="text-nowrap">
  273. {deletionModeButtons}
  274. {allSelectCheck}
  275. </div>
  276. </div>
  277. <div className="page-list">
  278. <ul className="page-list-ul page-list-ul-flat nav nav-pills">{listView}</ul>
  279. </div>
  280. </nav>
  281. </div>
  282. <div className="col-lg-8 search-result-content" id="search-result-content">
  283. <SearchResultList pages={this.props.pages} searchingKeyword={this.props.searchingKeyword} />
  284. </div>
  285. </div>
  286. <DeletePageListModal
  287. isShown={this.state.isDeleteConfirmModalShown}
  288. pages={Array.from(this.state.selectedPages)}
  289. errorMessage={this.state.errorMessageForDeleting}
  290. cancel={this.closeDeleteConfirmModal}
  291. confirmedToDelete={this.deleteSelectedPages}
  292. isDeleteCompletely={this.state.isDeleteCompletely}
  293. toggleDeleteCompletely={this.toggleDeleteCompletely}
  294. />
  295. </div> // content-main
  296. );
  297. }
  298. }
  299. /**
  300. * Wrapper component for using unstated
  301. */
  302. const SearchResultWrapper = withUnstatedContainers(SearchResult, [AppContainer]);
  303. SearchResult.propTypes = {
  304. appContainer: PropTypes.instanceOf(AppContainer).isRequired,
  305. t: PropTypes.func.isRequired, // i18next
  306. pages: PropTypes.array.isRequired,
  307. searchingKeyword: PropTypes.string.isRequired,
  308. searchResultMeta: PropTypes.object.isRequired,
  309. searchError: PropTypes.object,
  310. tree: PropTypes.string,
  311. };
  312. SearchResult.defaultProps = {
  313. searchError: null,
  314. };
  315. export default withTranslation()(SearchResultWrapper);