controls.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  1. import React from 'react';
  2. import ErrorsModal from 'misago/components/threads/moderation/errors-list'; // jshint ignore:line
  3. import MergeThreads from 'misago/components/threads/moderation/merge'; // jshint ignore:line
  4. import MoveThreads from 'misago/components/threads/moderation/move'; // jshint ignore:line
  5. import * as select from 'misago/reducers/selection'; // jshint ignore:line
  6. import ajax from 'misago/services/ajax'; // jshint ignore:line
  7. import modal from 'misago/services/modal'; // jshint ignore:line
  8. import snackbar from 'misago/services/snackbar'; // jshint ignore:line
  9. import store from 'misago/services/store'; // jshint ignore:line
  10. import Countdown from 'misago/utils/countdown'; // jshint ignore:line
  11. export default class extends React.Component {
  12. /* jshint ignore:start */
  13. callApi = (ops, successMessage, onSuccess=null) => {
  14. // freeze threads
  15. this.props.threads.forEach((thread) => {
  16. this.props.freezeThread(thread.id);
  17. });
  18. // list ids
  19. const ids = this.props.threads.map((thread) => {
  20. return thread.id;
  21. });
  22. // always return current acl
  23. ops.push({op: 'add', path: 'acl', value: true});
  24. ajax.patch(this.props.api, { ids, ops }).then(
  25. (data) => {
  26. // unfreeze
  27. this.props.threads.forEach((thread) => {
  28. this.props.freezeThread(thread.id);
  29. });
  30. // update threads
  31. data.forEach((thread) => {
  32. this.props.updateThread(thread);
  33. });
  34. // show success message and call callback
  35. snackbar.success(successMessage);
  36. if (onSuccess) {
  37. onSuccess();
  38. }
  39. },
  40. (rejection) => {
  41. // unfreeze
  42. this.props.threads.forEach((thread) => {
  43. this.props.freezeThread(thread.id);
  44. });
  45. // escape on non-400 error
  46. if (rejection.status !== 400) {
  47. return snackbar.apiError(rejection);
  48. }
  49. // build errors list
  50. let errors = [];
  51. let threadsMap = {}
  52. this.props.threads.forEach((thread) => {
  53. threadsMap[thread.id] = thread;
  54. });
  55. rejection.forEach(({id, detail }) => {
  56. if (typeof threadsMap[id] !== 'undefined') {
  57. errors.push({
  58. errors: detail,
  59. thread: threadsMap[id]
  60. });
  61. }
  62. });
  63. modal.show(
  64. <ErrorsModal errors={errors} />
  65. );
  66. }
  67. );
  68. };
  69. pinGlobally = () => {
  70. this.callApi([
  71. {
  72. op: 'replace',
  73. path: 'weight',
  74. value: 2
  75. }
  76. ], gettext("Selected threads were pinned globally."));
  77. };
  78. pinLocally = () => {
  79. this.callApi([
  80. {
  81. op: 'replace',
  82. path: 'weight',
  83. value: 1
  84. }
  85. ], gettext("Selected threads were pinned locally."));
  86. };
  87. unpin = () => {
  88. this.callApi([
  89. {
  90. op: 'replace',
  91. path: 'weight',
  92. value: 0
  93. }
  94. ], gettext("Selected threads were unpinned."));
  95. };
  96. approve = () => {
  97. this.callApi([
  98. {
  99. op: 'replace',
  100. path: 'is-unapproved',
  101. value: false
  102. }
  103. ], gettext("Selected threads were approved."));
  104. };
  105. open = () => {
  106. this.callApi([
  107. {
  108. op: 'replace',
  109. path: 'is-closed',
  110. value: false
  111. }
  112. ], gettext("Selected threads were opened."));
  113. };
  114. close = () => {
  115. this.callApi([
  116. {
  117. op: 'replace',
  118. path: 'is-closed',
  119. value: true
  120. }
  121. ], gettext("Selected threads were closed."));
  122. };
  123. unhide = () => {
  124. this.callApi([
  125. {
  126. op: 'replace',
  127. path: 'is-hidden',
  128. value: false
  129. }
  130. ], gettext("Selected threads were unhidden."));
  131. };
  132. hide = () => {
  133. this.callApi([
  134. {
  135. op: 'replace',
  136. path: 'is-hidden',
  137. value: true
  138. }
  139. ], gettext("Selected threads were hidden."));
  140. };
  141. move = () => {
  142. modal.show(
  143. <MoveThreads
  144. callApi={this.callApi}
  145. categories={this.props.categories}
  146. categoriesMap={this.props.categoriesMap}
  147. route={this.props.route}
  148. user={this.props.user}
  149. />
  150. );
  151. };
  152. merge = () => {
  153. const errors = [];
  154. this.props.threads.forEach((thread) => {
  155. if (!thread.acl.can_merge) {
  156. errors.append({
  157. 'id': thread.id,
  158. 'title': thread.title,
  159. 'errors': [
  160. gettext("You don't have permission to merge this thread with others.")
  161. ]
  162. });
  163. }
  164. });
  165. if (this.props.threads.length < 2) {
  166. snackbar.info(
  167. gettext("You have to select at least two threads to merge."));
  168. } else if (errors.length) {
  169. modal.show(<ErrorsModal errors={errors} />);
  170. return;
  171. } else {
  172. modal.show(<MergeThreads {...this.props} />);
  173. }
  174. };
  175. delete = () => {
  176. if (!confirm(gettext("Are you sure you want to delete selected threads?"))) {
  177. return;
  178. }
  179. this.props.threads.map((thread) => {
  180. this.props.freezeThread(thread.id);
  181. });
  182. const ids = this.props.threads.map((thread) => { return thread.id; });
  183. ajax.delete(this.props.api, ids).then(() => {
  184. this.props.threads.map((thread) => {
  185. this.props.freezeThread(thread.id);
  186. this.props.deleteThread(thread);
  187. });
  188. snackbar.success(gettext("Selected threads were deleted."));
  189. }, (rejection) => {
  190. if (rejection.status === 400) {
  191. const failedThreads = rejection.map((thread) => { return thread.id; });
  192. this.props.threads.map((thread) => {
  193. this.props.freezeThread(thread.id);
  194. if (failedThreads.indexOf(thread.id) === -1) {
  195. this.props.deleteThread(thread);
  196. }
  197. });
  198. modal.show(<ErrorsModal errors={rejection} />);
  199. } else {
  200. snackbar.apiError(rejection);
  201. }
  202. });
  203. };
  204. /* jshint ignore:end */
  205. getPinGloballyButton() {
  206. if (!this.props.moderation.can_pin_globally) return null;
  207. /* jshint ignore:start */
  208. return (
  209. <li>
  210. <button
  211. className="btn btn-link"
  212. onClick={this.pinGlobally}
  213. type="button"
  214. >
  215. <span className="material-icon">
  216. bookmark
  217. </span>
  218. {gettext("Pin threads globally")}
  219. </button>
  220. </li>
  221. );
  222. /* jshint ignore:end */
  223. }
  224. getPinLocallyButton() {
  225. if (!this.props.moderation.can_pin) return null;
  226. /* jshint ignore:start */
  227. return (
  228. <li>
  229. <button
  230. className="btn btn-link"
  231. onClick={this.pinLocally}
  232. type="button"
  233. >
  234. <span className="material-icon">
  235. bookmark_border
  236. </span>
  237. {gettext("Pin threads locally")}
  238. </button>
  239. </li>
  240. );
  241. /* jshint ignore:end */
  242. }
  243. getUnpinButton() {
  244. if (!this.props.moderation.can_pin) return null;
  245. /* jshint ignore:start */
  246. return (
  247. <li>
  248. <button
  249. className="btn btn-link"
  250. onClick={this.unpin}
  251. type="button"
  252. >
  253. <span className="material-icon">
  254. panorama_fish_eye
  255. </span>
  256. {gettext("Unpin threads")}
  257. </button>
  258. </li>
  259. );
  260. /* jshint ignore:end */
  261. }
  262. getMoveButton() {
  263. if (!this.props.moderation.can_move) return null;
  264. /* jshint ignore:start */
  265. return (
  266. <li>
  267. <button
  268. className="btn btn-link"
  269. onClick={this.move}
  270. type="button"
  271. >
  272. <span className="material-icon">
  273. arrow_forward
  274. </span>
  275. {gettext("Move threads")}
  276. </button>
  277. </li>
  278. );
  279. /* jshint ignore:end */
  280. }
  281. getMergeButton() {
  282. if (!this.props.moderation.can_merge) return null;
  283. /* jshint ignore:start */
  284. return (
  285. <li>
  286. <button
  287. className="btn btn-link"
  288. onClick={this.merge}
  289. type="button"
  290. >
  291. <span className="material-icon">
  292. call_merge
  293. </span>
  294. {gettext("Merge threads")}
  295. </button>
  296. </li>
  297. );
  298. /* jshint ignore:end */
  299. }
  300. getApproveButton() {
  301. if (!this.props.moderation.can_approve) return null;
  302. /* jshint ignore:start */
  303. return (
  304. <li>
  305. <button
  306. className="btn btn-link"
  307. onClick={this.approve}
  308. type="button"
  309. >
  310. <span className="material-icon">
  311. done
  312. </span>
  313. {gettext("Approve threads")}
  314. </button>
  315. </li>
  316. );
  317. /* jshint ignore:end */
  318. }
  319. getOpenButton() {
  320. if (!this.props.moderation.can_close) return null;
  321. /* jshint ignore:start */
  322. return (
  323. <li>
  324. <button
  325. className="btn btn-link"
  326. onClick={this.open}
  327. type="button"
  328. >
  329. <span className="material-icon">
  330. lock_open
  331. </span>
  332. {gettext("Open threads")}
  333. </button>
  334. </li>
  335. );
  336. /* jshint ignore:end */
  337. }
  338. getCloseButton() {
  339. if (!this.props.moderation.can_close) return null;
  340. /* jshint ignore:start */
  341. return (
  342. <li>
  343. <button
  344. className="btn btn-link"
  345. onClick={this.close}
  346. type="button"
  347. >
  348. <span className="material-icon">
  349. lock_outline
  350. </span>
  351. {gettext("Close threads")}
  352. </button>
  353. </li>
  354. );
  355. /* jshint ignore:end */
  356. }
  357. getUnhideButton() {
  358. if (!this.props.moderation.can_unhide) return null;
  359. /* jshint ignore:start */
  360. return (
  361. <li>
  362. <button
  363. className="btn btn-link"
  364. onClick={this.unhide}
  365. type="button"
  366. >
  367. <span className="material-icon">
  368. visibility
  369. </span>
  370. {gettext("Unhide threads")}
  371. </button>
  372. </li>
  373. );
  374. /* jshint ignore:end */
  375. }
  376. getHideButton() {
  377. if (!this.props.moderation.can_hide) return null;
  378. /* jshint ignore:start */
  379. return (
  380. <li>
  381. <button
  382. onClick={this.hide}
  383. type="button"
  384. className="btn btn-link"
  385. >
  386. <span className="material-icon">
  387. visibility_off
  388. </span>
  389. {gettext("Hide threads")}
  390. </button>
  391. </li>
  392. );
  393. /* jshint ignore:end */
  394. }
  395. getDeleteButton() {
  396. if (!this.props.moderation.can_delete) return null;
  397. /* jshint ignore:start */
  398. return (
  399. <li>
  400. <button
  401. className="btn btn-link"
  402. onClick={this.delete}
  403. type="button"
  404. >
  405. <span className="material-icon">
  406. clear
  407. </span>
  408. {gettext("Delete threads")}
  409. </button>
  410. </li>
  411. );
  412. /* jshint ignore:end */
  413. }
  414. render() {
  415. /* jshint ignore:start */
  416. return <ul className={this.props.className}>
  417. {this.getPinGloballyButton()}
  418. {this.getPinLocallyButton()}
  419. {this.getUnpinButton()}
  420. {this.getMoveButton()}
  421. {this.getMergeButton()}
  422. {this.getApproveButton()}
  423. {this.getOpenButton()}
  424. {this.getCloseButton()}
  425. {this.getUnhideButton()}
  426. {this.getHideButton()}
  427. {this.getDeleteButton()}
  428. </ul>;
  429. /* jshint ignore:end */
  430. }
  431. }