start.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. import React from 'react'; //jshint ignore:line
  2. import CategorySelect from 'misago/components/category-select'; //jshint ignore:line
  3. import Editor from 'misago/components/editor'; //jshint ignore:line
  4. import Form from 'misago/components/form';
  5. import Container from './utils/container'; //jshint ignore:line
  6. import Loader from './utils/loader'; //jshint ignore:line
  7. import Message from './utils/message'; //jshint ignore:line
  8. import Options from './utils/options'; //jshint ignore:line
  9. import * as attachments from './utils/attachments'; //jshint ignore:line
  10. import { getPostValidators, getTitleValidators } from './utils/validators';
  11. import ajax from 'misago/services/ajax';
  12. import posting from 'misago/services/posting'; //jshint ignore:line
  13. import snackbar from 'misago/services/snackbar';
  14. export default class extends Form {
  15. constructor(props) {
  16. super(props);
  17. this.state = {
  18. isReady: false,
  19. isLoading: false,
  20. isErrored: false,
  21. showOptions: false,
  22. categoryOptions: null,
  23. title: '',
  24. category: props.category || null,
  25. categories: [],
  26. post: '',
  27. attachments: [],
  28. close: false,
  29. hide: false,
  30. pin: 0,
  31. validators: {
  32. title: getTitleValidators(),
  33. post: getPostValidators()
  34. },
  35. errors: {}
  36. };
  37. }
  38. componentDidMount() {
  39. ajax.get(this.props.config).then(this.loadSuccess, this.loadError);
  40. }
  41. /* jshint ignore:start */
  42. loadSuccess = (data) => {
  43. let category = null;
  44. let showOptions = false;
  45. let categoryOptions = null;
  46. // hydrate categories, extract posting options
  47. const categories = data.map((item) => {
  48. // pick first category that allows posting and if it may, override it with initial one
  49. if (item.post !== false && (!category || item.id == this.state.category)) {
  50. category = item.id;
  51. categoryOptions = item.post;
  52. }
  53. if (item.post && (item.post.close || item.post.hide || item.post.pin)) {
  54. showOptions = true;
  55. }
  56. return Object.assign(item, {
  57. disabled: item.post === false,
  58. label: item.name,
  59. value: item.id
  60. });
  61. });
  62. this.setState({
  63. isReady: true,
  64. showOptions,
  65. categories,
  66. category,
  67. categoryOptions
  68. });
  69. };
  70. loadError = (rejection) => {
  71. this.setState({
  72. isErrored: rejection.detail
  73. });
  74. };
  75. onCancel = () => {
  76. const cancel = confirm(gettext("Are you sure you want to discard thread?"));
  77. if (cancel) {
  78. posting.close();
  79. }
  80. };
  81. onTitleChange = (event) => {
  82. this.changeValue('title', event.target.value);
  83. };
  84. onCategoryChange = (event) => {
  85. const category = this.state.categories.find((item) => {
  86. return event.target.value == item.value;
  87. });
  88. // if selected pin is greater than allowed, reduce it
  89. let pin = this.state.pin;
  90. if (category.post.pin && category.post.pin < pin) {
  91. pin = category.post.pin;
  92. }
  93. this.setState({
  94. category: category.id,
  95. categoryOptions: category.post,
  96. pin
  97. });
  98. };
  99. onPostChange = (event) => {
  100. this.changeValue('post', event.target.value);
  101. };
  102. onAttachmentsChange = (attachments) => {
  103. this.setState({
  104. attachments
  105. });
  106. };
  107. onClose = () => {
  108. this.changeValue('close', true);
  109. };
  110. onOpen = () => {
  111. this.changeValue('close', false);
  112. };
  113. onPinGlobally = () => {
  114. this.changeValue('pin', 2);
  115. };
  116. onPinLocally = () => {
  117. this.changeValue('pin', 1);
  118. };
  119. onUnpin = () => {
  120. this.changeValue('pin', 0);
  121. };
  122. onHide = () => {
  123. this.changeValue('hide', true);
  124. };
  125. onUnhide = () => {
  126. this.changeValue('hide', false);
  127. };
  128. /* jshint ignore:end */
  129. clean() {
  130. if (!this.state.title.trim().length) {
  131. snackbar.error(gettext("You have to enter thread title."));
  132. return false;
  133. }
  134. if (!this.state.post.trim().length) {
  135. snackbar.error(gettext("You have to enter a message."));
  136. return false;
  137. }
  138. const errors = this.validate();
  139. if (errors.title) {
  140. snackbar.error(errors.title[0]);
  141. return false;
  142. }
  143. if (errors.post) {
  144. snackbar.error(errors.post[0]);
  145. return false;
  146. }
  147. return true;
  148. }
  149. send() {
  150. return ajax.post(this.props.submit, {
  151. title: this.state.title,
  152. category: this.state.category,
  153. post: this.state.post,
  154. attachments: attachments.clean(this.state.attachments),
  155. close: this.state.close,
  156. hide: this.state.hide,
  157. pin: this.state.pin
  158. });
  159. }
  160. handleSuccess(success) {
  161. snackbar.success(gettext("Your thread has been posted."));
  162. window.location = success.url;
  163. // keep form loading
  164. this.setState({
  165. 'isLoading': true
  166. });
  167. }
  168. handleError(rejection) {
  169. if (rejection.status === 400) {
  170. const errors = [].concat(
  171. rejection.non_field_errors || [],
  172. rejection.category || [],
  173. rejection.title || [],
  174. rejection.post || []
  175. );
  176. snackbar.error(errors[0]);
  177. } else {
  178. snackbar.apiError(rejection);
  179. }
  180. }
  181. render() {
  182. /* jshint ignore:start */
  183. if (this.state.isErrored) {
  184. return (
  185. <Message message={this.state.isErrored} />
  186. );
  187. }
  188. if (!this.state.isReady) {
  189. return (
  190. <Loader />
  191. );
  192. }
  193. let columns = 0;
  194. if (this.state.categoryOptions.close) columns += 1;
  195. if (this.state.categoryOptions.hide) columns += 1;
  196. if (this.state.categoryOptions.pin) columns += 1;
  197. let titleStyle = null;
  198. if (columns === 1) {
  199. titleStyle = 'col-sm-6';
  200. } else {
  201. titleStyle = 'col-sm-8';
  202. }
  203. if (columns === 3) {
  204. titleStyle += ' col-md-6'
  205. } else if (columns) {
  206. titleStyle += ' col-md-7'
  207. } else {
  208. titleStyle += ' col-md-9'
  209. }
  210. return (
  211. <Container className="posting-form" withFirstRow={true}>
  212. <form onSubmit={this.handleSubmit}>
  213. <div className="row first-row">
  214. <div className={titleStyle}>
  215. <input
  216. className="form-control"
  217. disabled={this.state.isLoading}
  218. onChange={this.onTitleChange}
  219. placeholder={gettext("Thread title")}
  220. type="text"
  221. value={this.state.title}
  222. />
  223. </div>
  224. <div className='col-xs-12 col-sm-4 col-md-3 xs-margin-top'>
  225. <CategorySelect
  226. choices={this.state.categories}
  227. disabled={this.state.isLoading}
  228. onChange={this.onCategoryChange}
  229. value={this.state.category}
  230. />
  231. </div>
  232. <Options
  233. close={this.state.close}
  234. columns={columns}
  235. disabled={this.state.isLoading}
  236. hide={this.state.hide}
  237. onClose={this.onClose}
  238. onHide={this.onHide}
  239. onOpen={this.onOpen}
  240. onPinGlobally={this.onPinGlobally}
  241. onPinLocally={this.onPinLocally}
  242. onUnhide={this.onUnhide}
  243. onUnpin={this.onUnpin}
  244. options={this.state.categoryOptions}
  245. pin={this.state.pin}
  246. showOptions={this.state.showOptions}
  247. />
  248. </div>
  249. <div className="row">
  250. <div className="col-md-12">
  251. <Editor
  252. attachments={this.state.attachments}
  253. loading={this.state.isLoading}
  254. onAttachmentsChange={this.onAttachmentsChange}
  255. onCancel={this.onCancel}
  256. onChange={this.onPostChange}
  257. submitLabel={gettext("Post thread")}
  258. value={this.state.post}
  259. />
  260. </div>
  261. </div>
  262. </form>
  263. </Container>
  264. );
  265. /* jshint ignore:end */
  266. }
  267. }