start.js 7.3 KB

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