SearchResult.jsx 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  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 { createSubscribedElement } 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. * toggle checkbox and add (or delete from) selected pages list
  38. *
  39. * @param {any} page
  40. * @memberof SearchResult
  41. */
  42. toggleCheckbox(page) {
  43. if (this.state.selectedPages.has(page)) {
  44. this.state.selectedPages.delete(page);
  45. }
  46. else {
  47. this.state.selectedPages.add(page);
  48. }
  49. this.setState({ isDeleteConfirmModalShown: false });
  50. this.setState({ selectedPages: this.state.selectedPages });
  51. }
  52. /**
  53. * check and return is all pages selected for delete?
  54. *
  55. * @returns all pages selected (or not)
  56. * @memberof SearchResult
  57. */
  58. isAllSelected() {
  59. return this.state.selectedPages.size === this.props.pages.length;
  60. }
  61. /**
  62. * handle checkbox clicking that all pages select for delete
  63. *
  64. * @memberof SearchResult
  65. */
  66. handleAllSelect() {
  67. if (this.isAllSelected()) {
  68. this.state.selectedPages.clear();
  69. }
  70. else {
  71. this.state.selectedPages.clear();
  72. this.props.pages.map((page) => {
  73. this.state.selectedPages.add(page);
  74. return;
  75. });
  76. }
  77. this.setState({ selectedPages: this.state.selectedPages });
  78. }
  79. /**
  80. * change deletion mode
  81. *
  82. * @memberof SearchResult
  83. */
  84. handleDeletionModeChange() {
  85. this.state.selectedPages.clear();
  86. this.setState({ deletionMode: !this.state.deletionMode });
  87. }
  88. /**
  89. * toggle check delete completely
  90. *
  91. * @memberof SearchResult
  92. */
  93. toggleDeleteCompletely() {
  94. // request で completely が undefined でないと指定アリと見なされるため
  95. this.setState({ isDeleteCompletely: this.state.isDeleteCompletely ? undefined : true });
  96. }
  97. /**
  98. * delete selected pages
  99. *
  100. * @memberof SearchResult
  101. */
  102. deleteSelectedPages() {
  103. const deleteCompletely = this.state.isDeleteCompletely;
  104. Promise.all(Array.from(this.state.selectedPages).map((page) => {
  105. return new Promise((resolve, reject) => {
  106. const pageId = page._id;
  107. const revisionId = page.revision._id;
  108. this.props.appContainer.apiPost('/pages.remove', { page_id: pageId, revision_id: revisionId, completely: deleteCompletely })
  109. .then((res) => {
  110. if (res.ok) {
  111. this.state.selectedPages.delete(page);
  112. return resolve();
  113. }
  114. return reject();
  115. })
  116. .catch((err) => {
  117. console.log(err.message); // eslint-disable-line no-console
  118. this.setState({ errorMessageForDeleting: err.message });
  119. return reject();
  120. });
  121. });
  122. }))
  123. .then(() => {
  124. window.location.reload();
  125. })
  126. .catch((err) => {
  127. toastr.error(err, 'Error occured', {
  128. closeButton: true,
  129. progressBar: true,
  130. newestOnTop: false,
  131. showDuration: '100',
  132. hideDuration: '100',
  133. timeOut: '3000',
  134. });
  135. });
  136. }
  137. /**
  138. * open confirm modal for page selection delete
  139. *
  140. * @memberof SearchResult
  141. */
  142. showDeleteConfirmModal() {
  143. this.setState({ isDeleteConfirmModalShown: true });
  144. }
  145. /**
  146. * close confirm modal for page selection delete
  147. *
  148. * @memberof SearchResult
  149. */
  150. closeDeleteConfirmModal() {
  151. this.setState({
  152. isDeleteConfirmModalShown: false,
  153. errorMessageForDeleting: undefined,
  154. });
  155. }
  156. render() {
  157. const { t } = this.props;
  158. if (this.isError()) {
  159. return (
  160. <div className="content-main">
  161. <i className="searcing fa fa-warning"></i> Error on searching.
  162. </div>
  163. );
  164. }
  165. if (this.isNotSearchedYet()) {
  166. return <div />;
  167. }
  168. if (this.isNotFound()) {
  169. let under = '';
  170. if (this.props.tree != null) {
  171. under = ` under "${this.props.tree}"`;
  172. }
  173. return (
  174. <div className="content-main">
  175. <i className="icon-fw icon-info" /> No page found with &quot;{this.props.searchingKeyword}&quot;{under}
  176. </div>
  177. );
  178. }
  179. let deletionModeButtons = '';
  180. let allSelectCheck = '';
  181. if (this.state.deletionMode) {
  182. deletionModeButtons = (
  183. <div className="btn-group">
  184. <button type="button" className="btn btn-rounded btn-default btn-xs" onClick={() => { return this.handleDeletionModeChange() }}>
  185. <i className="icon-ban" /> {t('search_result.cancel')}
  186. </button>
  187. <button
  188. type="button"
  189. className="btn btn-rounded btn-danger btn-xs"
  190. onClick={() => { return this.showDeleteConfirmModal() }}
  191. disabled={this.state.selectedPages.size === 0}
  192. >
  193. <i className="icon-trash" /> {t('search_result.delete')}
  194. </button>
  195. </div>
  196. );
  197. allSelectCheck = (
  198. <div>
  199. <label>
  200. <input
  201. type="checkbox"
  202. onClick={() => { return this.handleAllSelect() }}
  203. checked={this.isAllSelected()}
  204. />
  205. {t('search_result.check_all')}
  206. </label>
  207. </div>
  208. );
  209. }
  210. else {
  211. deletionModeButtons = (
  212. <div className="btn-group">
  213. <button type="button" className="btn btn-default btn-rounded btn-xs" onClick={() => { return this.handleDeletionModeChange() }}>
  214. <i className="ti-check-box" /> {t('search_result.deletion_mode_btn_lavel')}
  215. </button>
  216. </div>
  217. );
  218. }
  219. const listView = this.props.pages.map((page) => {
  220. const pageId = `#${page._id}`;
  221. return (
  222. <Page
  223. page={page}
  224. linkTo={pageId}
  225. key={page._id}
  226. >
  227. { this.state.deletionMode
  228. && (
  229. <input
  230. type="checkbox"
  231. className="search-result-list-delete-checkbox"
  232. value={pageId}
  233. checked={this.state.selectedPages.has(page)}
  234. onClick={() => { return this.toggleCheckbox(page) }}
  235. />
  236. )
  237. }
  238. <div className="page-list-option">
  239. <a href={page.path}><i className="icon-login" /></a>
  240. </div>
  241. </Page>
  242. );
  243. });
  244. // TODO あとでなんとかする
  245. setTimeout(() => {
  246. $('#search-result-list > nav').affix({ offset: { top: 50 } });
  247. }, 1200);
  248. /*
  249. UI あとで考える
  250. <span className="search-result-meta">Found: {this.props.searchResultMeta.total} pages with "{this.props.searchingKeyword}"</span>
  251. */
  252. return (
  253. <div className="content-main">
  254. <div className="search-result row" id="search-result">
  255. <div className="col-md-4 hidden-xs hidden-sm page-list search-result-list" id="search-result-list">
  256. <nav data-spy="affix" data-offset-top="50">
  257. <div className="pull-right">
  258. {deletionModeButtons}
  259. {allSelectCheck}
  260. </div>
  261. <div className="search-result-meta">
  262. <i className="icon-magnifier" />
  263. {t('search_result.result_meta', { total: this.props.searchResultMeta.total, keyword: this.props.searchingKeyword })}
  264. </div>
  265. <div className="clearfix"></div>
  266. <div className="page-list">
  267. <ul className="page-list-ul page-list-ul-flat nav">{listView}</ul>
  268. </div>
  269. </nav>
  270. </div>
  271. <div className="col-md-8 search-result-content" id="search-result-content">
  272. <SearchResultList pages={this.props.pages} searchingKeyword={this.props.searchingKeyword} />
  273. </div>
  274. </div>
  275. <DeletePageListModal
  276. isShown={this.state.isDeleteConfirmModalShown}
  277. pages={Array.from(this.state.selectedPages)}
  278. errorMessage={this.state.errorMessageForDeleting}
  279. cancel={this.closeDeleteConfirmModal}
  280. confirmedToDelete={this.deleteSelectedPages}
  281. toggleDeleteCompletely={this.toggleDeleteCompletely}
  282. />
  283. </div> // content-main
  284. );
  285. }
  286. }
  287. /**
  288. * Wrapper component for using unstated
  289. */
  290. const SearchResultWrapper = (props) => {
  291. return createSubscribedElement(SearchResult, props, [AppContainer]);
  292. };
  293. SearchResult.propTypes = {
  294. appContainer: PropTypes.instanceOf(AppContainer).isRequired,
  295. t: PropTypes.func.isRequired, // i18next
  296. pages: PropTypes.array.isRequired,
  297. searchingKeyword: PropTypes.string.isRequired,
  298. searchResultMeta: PropTypes.object.isRequired,
  299. searchError: PropTypes.object,
  300. tree: PropTypes.string,
  301. };
  302. SearchResult.defaultProps = {
  303. searchError: null,
  304. };
  305. export default withTranslation()(SearchResultWrapper);