SearchResult.js 8.9 KB

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