Thread.jsx 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. import React from 'react'
  2. import i18n from '../i18n.js'
  3. import { debug } from '../helper.js'
  4. import {
  5. addAllResourceI18n,
  6. handleFetchResult,
  7. generateAvatarFromPublicName,
  8. PopinFixed,
  9. PopinFixedHeader,
  10. PopinFixedOption,
  11. PopinFixedContent,
  12. Timeline,
  13. SelectStatus,
  14. ArchiveDeleteContent
  15. } from 'tracim_frontend_lib'
  16. import {
  17. getThreadContent,
  18. getThreadComment,
  19. postThreadNewComment,
  20. putThreadStatus,
  21. putThreadContent,
  22. putThreadIsArchived,
  23. putThreadIsDeleted,
  24. putThreadRestoreArchived,
  25. putThreadRestoreDeleted,
  26. putThreadRead
  27. } from '../action.async.js'
  28. class Thread extends React.Component {
  29. constructor (props) {
  30. super(props)
  31. this.state = {
  32. appName: 'thread',
  33. isVisible: true,
  34. config: props.data ? props.data.config : debug.config,
  35. loggedUser: props.data ? props.data.loggedUser : debug.loggedUser,
  36. content: props.data ? props.data.content : debug.content,
  37. listMessage: props.data ? [] : [], // debug.listMessage,
  38. newComment: '',
  39. timelineWysiwyg: false
  40. }
  41. // i18n has been init, add resources from frontend
  42. addAllResourceI18n(i18n, this.state.config.translation)
  43. i18n.changeLanguage(this.state.loggedUser.lang)
  44. document.addEventListener('appCustomEvent', this.customEventReducer)
  45. }
  46. customEventReducer = ({ detail: { type, data } }) => { // action: { type: '', data: {} }
  47. switch (type) {
  48. case 'thread_showApp':
  49. console.log('%c<Thread> Custom event', 'color: #28a745', type, data)
  50. this.setState({isVisible: true})
  51. break
  52. case 'thread_hideApp':
  53. console.log('%c<Thread> Custom event', 'color: #28a745', type, data)
  54. this.setState({isVisible: false})
  55. break
  56. case 'thread_reloadContent':
  57. console.log('%c<Thread> Custom event', 'color: #28a745', type, data)
  58. this.setState(prev => ({content: {...prev.content, ...data}, isVisible: true}))
  59. break
  60. case 'allApp_changeLang':
  61. console.log('%c<Thread> Custom event', 'color: #28a745', type, data)
  62. this.setState(prev => ({
  63. loggedUser: {
  64. ...prev.loggedUser,
  65. lang: data
  66. }
  67. }))
  68. i18n.changeLanguage(data)
  69. break
  70. }
  71. }
  72. componentDidMount () {
  73. console.log('%c<Thread> did Mount', `color: ${this.state.config.hexcolor}`)
  74. this.loadContent()
  75. }
  76. componentDidUpdate (prevProps, prevState) {
  77. const { state } = this
  78. console.log('%c<Thread> did Update', `color: ${this.state.config.hexcolor}`, prevState, state)
  79. if (!prevState.content || !state.content) return
  80. if (prevState.content.content_id !== state.content.content_id) this.loadContent()
  81. if (!prevState.timelineWysiwyg && state.timelineWysiwyg) wysiwyg('#wysiwygTimelineComment', this.handleChangeNewComment)
  82. else if (prevState.timelineWysiwyg && !state.timelineWysiwyg) tinymce.remove('#wysiwygTimelineComment')
  83. }
  84. componentWillUnmount () {
  85. console.log('%c<Thread> will Unmount', `color: ${this.state.config.hexcolor}`)
  86. document.removeEventListener('appCustomEvent', this.customEventReducer)
  87. }
  88. loadContent = async () => {
  89. const { loggedUser, content, config } = this.state
  90. if (content.content_id === '-1') return // debug case
  91. const fetchResultThread = getThreadContent(loggedUser, config.apiUrl, content.workspace_id, content.content_id)
  92. const fetchResultThreadComment = getThreadComment(loggedUser, config.apiUrl, content.workspace_id, content.content_id)
  93. Promise.all([
  94. handleFetchResult(await fetchResultThread),
  95. handleFetchResult(await fetchResultThreadComment)
  96. ])
  97. .then(([resThread, resComment]) => {
  98. this.setState({
  99. content: resThread.body,
  100. listMessage: resComment.body.map(c => ({
  101. ...c,
  102. timelineType: 'comment',
  103. created: (new Date(c.created)).toLocaleString(),
  104. author: {
  105. ...c.author,
  106. avatar_url: c.author.avatar_url
  107. ? c.author.avatar_url
  108. : generateAvatarFromPublicName(c.author.public_name)
  109. }
  110. }))
  111. })
  112. putThreadRead(loggedUser, config.apiUrl, content.workspace_id, content.content_id)
  113. })
  114. .catch(e => console.log('Error loading Thread data.', e))
  115. }
  116. handleClickBtnCloseApp = () => {
  117. this.setState({ isVisible: false })
  118. GLOBAL_dispatchEvent({type: 'appClosed', data: {}}) // handled by tracim_front::src/container/WorkspaceContent.jsx
  119. }
  120. handleSaveEditTitle = async newTitle => {
  121. const { loggedUser, config, content } = this.state
  122. const fetchResultSaveThread = putThreadContent(loggedUser, config.apiUrl, content.workspace_id, content.content_id, newTitle)
  123. handleFetchResult(await fetchResultSaveThread)
  124. .then(resSave => {
  125. if (resSave.apiResponse.status === 200) {
  126. this.loadContent()
  127. GLOBAL_dispatchEvent({ type: 'refreshContentList', data: {} })
  128. } else {
  129. console.warn('Error saving threads. Result:', resSave, 'content:', content, 'config:', config)
  130. }
  131. })
  132. }
  133. handleChangeNewComment = e => {
  134. const newComment = e.target.value
  135. this.setState({newComment})
  136. }
  137. handleClickValidateNewCommentBtn = async () => {
  138. const { loggedUser, config, content, newComment } = this.state
  139. const fetchResultSaveNewComment = await postThreadNewComment(loggedUser, config.apiUrl, content.workspace_id, content.content_id, newComment)
  140. handleFetchResult(await fetchResultSaveNewComment)
  141. .then(resSave => {
  142. if (resSave.apiResponse.status === 200) {
  143. this.setState({newComment: ''})
  144. if (this.state.timelineWysiwyg) tinymce.get('wysiwygTimelineComment').setContent('')
  145. this.loadContent()
  146. } else {
  147. console.warn('Error saving thread comment. Result:', resSave, 'content:', content, 'config:', config)
  148. }
  149. })
  150. }
  151. handleToggleWysiwyg = () => this.setState(prev => ({timelineWysiwyg: !prev.timelineWysiwyg}))
  152. handleChangeStatus = async newStatus => {
  153. const { loggedUser, config, content } = this.state
  154. const fetchResultSaveEditStatus = putThreadStatus(loggedUser, config.apiUrl, content.workspace_id, content.content_id, newStatus)
  155. handleFetchResult(await fetchResultSaveEditStatus)
  156. .then(resSave => {
  157. if (resSave.status === 204) { // 204 no content so dont take status from resSave.apiResponse.status
  158. this.loadContent()
  159. } else {
  160. console.warn('Error saving thread comment. Result:', resSave, 'content:', content, 'config:', config)
  161. }
  162. })
  163. }
  164. handleClickArchive = async () => {
  165. const { loggedUser, config, content } = this.state
  166. const fetchResultArchive = await putThreadIsArchived(loggedUser, config.apiUrl, content.workspace_id, content.content_id)
  167. switch (fetchResultArchive.status) {
  168. case 204: this.setState(prev => ({content: {...prev.content, is_archived: true}})); break
  169. default: GLOBAL_dispatchEvent({
  170. type: 'addFlashMsg',
  171. data: {
  172. msg: this.props.t('Error while archiving thread'),
  173. type: 'warning',
  174. delay: undefined
  175. }
  176. })
  177. }
  178. }
  179. handleClickDelete = async () => {
  180. const { loggedUser, config, content } = this.state
  181. const fetchResultArchive = await putThreadIsDeleted(loggedUser, config.apiUrl, content.workspace_id, content.content_id)
  182. switch (fetchResultArchive.status) {
  183. case 204: this.setState(prev => ({content: {...prev.content, is_deleted: true}})); break
  184. default: GLOBAL_dispatchEvent({
  185. type: 'addFlashMsg',
  186. data: {
  187. msg: this.props.t('Error while deleting thread'),
  188. type: 'warning',
  189. delay: undefined
  190. }
  191. })
  192. }
  193. }
  194. handleClickRestoreArchived = async () => {
  195. const { loggedUser, config, content } = this.state
  196. const fetchResultRestore = await putThreadRestoreArchived(loggedUser, config.apiUrl, content.workspace_id, content.content_id)
  197. switch (fetchResultRestore.status) {
  198. case 204: this.setState(prev => ({content: {...prev.content, is_archived: false}})); break
  199. default: GLOBAL_dispatchEvent({
  200. type: 'addFlashMsg',
  201. data: {
  202. msg: this.props.t('Error while restoring thread'),
  203. type: 'warning',
  204. delay: undefined
  205. }
  206. })
  207. }
  208. }
  209. handleClickRestoreDeleted = async () => {
  210. const { loggedUser, config, content } = this.state
  211. const fetchResultRestore = await putThreadRestoreDeleted(loggedUser, config.apiUrl, content.workspace_id, content.content_id)
  212. switch (fetchResultRestore.status) {
  213. case 204: this.setState(prev => ({content: {...prev.content, is_deleted: false}})); break
  214. default: GLOBAL_dispatchEvent({
  215. type: 'addFlashMsg',
  216. data: {
  217. msg: this.props.t('Error while restoring thread'),
  218. type: 'warning',
  219. delay: undefined
  220. }
  221. })
  222. }
  223. }
  224. render () {
  225. const { config, isVisible, loggedUser, content, listMessage, newComment, timelineWysiwyg } = this.state
  226. if (!isVisible) return null
  227. return (
  228. <PopinFixed customClass={config.slug} customColor={config.hexcolor}>
  229. <PopinFixedHeader
  230. customClass={`${config.slug}__contentpage`}
  231. customColor={config.hexcolor}
  232. faIcon={config.faIcon}
  233. title={content.label}
  234. idRoleUserWorkspace={loggedUser.idRoleUserWorkspace}
  235. onClickCloseBtn={this.handleClickBtnCloseApp}
  236. onValidateChangeTitle={this.handleSaveEditTitle}
  237. />
  238. <PopinFixedOption
  239. customClass={`${config.slug}__contentpage`}
  240. customColor={config.hexcolor}
  241. i18n={i18n}
  242. >
  243. <div className='justify-content-end'>
  244. {loggedUser.idRoleUserWorkspace >= 2 &&
  245. <SelectStatus
  246. selectedStatus={config.availableStatuses.find(s => s.slug === content.status)}
  247. availableStatus={config.availableStatuses}
  248. onChangeStatus={this.handleChangeStatus}
  249. disabled={false}
  250. />
  251. }
  252. {loggedUser.idRoleUserWorkspace >= 4 &&
  253. <ArchiveDeleteContent
  254. customColor={config.hexcolor}
  255. onClickArchiveBtn={this.handleClickArchive}
  256. onClickDeleteBtn={this.handleClickDelete}
  257. disabled={false}
  258. />
  259. }
  260. </div>
  261. </PopinFixedOption>
  262. <PopinFixedContent customClass={`${config.slug}__contentpage`}>
  263. <Timeline
  264. customClass={`${config.slug}__contentpage`}
  265. customColor={config.hexcolor}
  266. loggedUser={loggedUser}
  267. timelineData={listMessage}
  268. newComment={newComment}
  269. disableComment={false}
  270. wysiwyg={timelineWysiwyg}
  271. onChangeNewComment={this.handleChangeNewComment}
  272. onClickValidateNewCommentBtn={this.handleClickValidateNewCommentBtn}
  273. onClickWysiwygBtn={this.handleToggleWysiwyg}
  274. onClickRevisionBtn={() => {}}
  275. shouldScrollToBottom
  276. showHeader={false}
  277. isArchived={content.is_archived}
  278. onClickRestoreArchived={this.handleClickRestoreArchived}
  279. isDeleted={content.is_deleted}
  280. onClickRestoreDeleted={this.handleClickRestoreDeleted}
  281. />
  282. </PopinFixedContent>
  283. </PopinFixed>
  284. )
  285. }
  286. }
  287. export default Thread