Thread.jsx 8.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  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. } from '../action.async.js'
  23. class Thread extends React.Component {
  24. constructor (props) {
  25. super(props)
  26. this.state = {
  27. appName: 'thread',
  28. isVisible: true,
  29. config: props.data ? props.data.config : debug.config,
  30. loggedUser: props.data ? props.data.loggedUser : debug.loggedUser,
  31. content: props.data ? props.data.content : debug.content,
  32. listMessage: props.data ? [] : [], // debug.listMessage,
  33. newComment: '',
  34. timelineWysiwyg: false
  35. }
  36. // i18n has been init, add resources from frontend
  37. addAllResourceI18n(i18n, this.state.config.translation)
  38. i18n.changeLanguage(this.state.loggedUser.lang)
  39. document.addEventListener('appCustomEvent', this.customEventReducer)
  40. }
  41. customEventReducer = ({ detail: { type, data } }) => { // action: { type: '', data: {} }
  42. switch (type) {
  43. case 'thread_showApp':
  44. console.log('%c<Thread> Custom event', 'color: #28a745', type, data)
  45. this.setState({isVisible: true})
  46. break
  47. case 'thread_hideApp':
  48. console.log('%c<Thread> Custom event', 'color: #28a745', type, data)
  49. this.setState({isVisible: false})
  50. break
  51. case 'thread_reloadContent':
  52. console.log('%c<Thread> Custom event', 'color: #28a745', type, data)
  53. this.setState(prev => ({content: {...prev.content, ...data}, isVisible: true}))
  54. break
  55. case 'allApp_changeLang':
  56. console.log('%c<Thread> Custom event', 'color: #28a745', type, data)
  57. this.setState(prev => ({
  58. loggedUser: {
  59. ...prev.loggedUser,
  60. lang: data
  61. }
  62. }))
  63. i18n.changeLanguage(data)
  64. break
  65. }
  66. }
  67. componentDidMount () {
  68. console.log('%c<Thread> did Mount', `color: ${this.state.config.hexcolor}`)
  69. this.loadContent()
  70. }
  71. componentDidUpdate (prevProps, prevState) {
  72. const { state } = this
  73. console.log('%c<Thread> did Mount', `color: ${this.state.config.hexcolor}`, prevState, state)
  74. if (!prevState.content || !state.content) return
  75. if (prevState.content.content_id !== state.content.content_id) this.loadContent()
  76. if (!prevState.timelineWysiwyg && state.timelineWysiwyg) wysiwyg('#wysiwygTimelineComment', this.handleChangeNewComment)
  77. else if (prevState.timelineWysiwyg && !state.timelineWysiwyg) tinymce.remove('#wysiwygTimelineComment')
  78. }
  79. componentWillUnmount () {
  80. console.log('%c<Thread> will Unmount', `color: ${this.state.config.hexcolor}`)
  81. document.removeEventListener('appCustomEvent', this.customEventReducer)
  82. }
  83. loadContent = async () => {
  84. const { loggedUser, content, config } = this.state
  85. if (content.content_id === '-1') return // debug case
  86. const fetchResultThread = getThreadContent(loggedUser, config.apiUrl, content.workspace_id, content.content_id)
  87. const fetchResultThreadComment = getThreadComment(loggedUser, config.apiUrl, content.workspace_id, content.content_id)
  88. Promise.all([
  89. handleFetchResult(await fetchResultThread),
  90. handleFetchResult(await fetchResultThreadComment)
  91. ])
  92. .then(([resThread, resComment]) => this.setState({
  93. content: resThread.body,
  94. listMessage: resComment.body.map(c => ({
  95. ...c,
  96. timelineType: 'comment',
  97. created: (new Date(c.created)).toLocaleString(),
  98. author: {
  99. ...c.author,
  100. avatar_url: c.author.avatar_url
  101. ? c.author.avatar_url
  102. : generateAvatarFromPublicName(c.author.public_name)
  103. }
  104. }))
  105. }))
  106. .catch(e => console.log('Error loading Thread data.', e))
  107. }
  108. handleClickBtnCloseApp = () => {
  109. this.setState({ isVisible: false })
  110. GLOBAL_dispatchEvent({type: 'appClosed', data: {}}) // handled by tracim_front::src/container/WorkspaceContent.jsx
  111. }
  112. handleSaveEditTitle = async newTitle => {
  113. const { loggedUser, config, content } = this.state
  114. const fetchResultSaveThread = putThreadContent(loggedUser, config.apiUrl, content.workspace_id, content.content_id, newTitle)
  115. handleFetchResult(await fetchResultSaveThread)
  116. .then(resSave => {
  117. if (resSave.apiResponse.status === 200) {
  118. this.loadContent()
  119. GLOBAL_dispatchEvent({ type: 'refreshContentList', data: {} })
  120. } else {
  121. console.warn('Error saving threads. Result:', resSave, 'content:', content, 'config:', config)
  122. }
  123. })
  124. }
  125. handleChangeNewComment = e => {
  126. const newComment = e.target.value
  127. this.setState({newComment})
  128. }
  129. handleClickValidateNewCommentBtn = async () => {
  130. const { loggedUser, config, content, newComment } = this.state
  131. const fetchResultSaveNewComment = await postThreadNewComment(loggedUser, config.apiUrl, content.workspace_id, content.content_id, newComment)
  132. handleFetchResult(await fetchResultSaveNewComment)
  133. .then(resSave => {
  134. if (resSave.apiResponse.status === 200) {
  135. this.setState({newComment: ''})
  136. if (this.state.timelineWysiwyg) tinymce.get('wysiwygTimelineComment').setContent('')
  137. this.loadContent()
  138. } else {
  139. console.warn('Error saving thread comment. Result:', resSave, 'content:', content, 'config:', config)
  140. }
  141. })
  142. }
  143. handleToggleWysiwyg = () => this.setState(prev => ({timelineWysiwyg: !prev.timelineWysiwyg}))
  144. handleChangeStatus = async newStatus => {
  145. const { loggedUser, config, content } = this.state
  146. const fetchResultSaveEditStatus = putThreadStatus(loggedUser, config.apiUrl, content.workspace_id, content.content_id, newStatus)
  147. handleFetchResult(await fetchResultSaveEditStatus)
  148. .then(resSave => {
  149. if (resSave.status !== 204) { // 204 no content so dont take status from resSave.apiResponse.status
  150. console.warn('Error saving thread comment. Result:', resSave, 'content:', content, 'config:', config)
  151. } else {
  152. this.loadContent()
  153. }
  154. })
  155. }
  156. handleClickArchive = () => console.log('archive nyi')
  157. handleClickDelete = () => console.log('delete nyi')
  158. render () {
  159. const { config, isVisible, loggedUser, content, listMessage, newComment, timelineWysiwyg } = this.state
  160. if (!isVisible) return null
  161. return (
  162. <PopinFixed
  163. customClass={config.slug}
  164. customColor={config.hexcolor}
  165. >
  166. <PopinFixedHeader
  167. customClass={`${config.slug}__contentpage`}
  168. customColor={config.hexcolor}
  169. faIcon={config.faIcon}
  170. title={content.label}
  171. onClickCloseBtn={this.handleClickBtnCloseApp}
  172. onValidateChangeTitle={this.handleSaveEditTitle}
  173. />
  174. <PopinFixedOption
  175. customClass={`${config.slug}__contentpage`}
  176. customColor={config.hexcolor}
  177. i18n={i18n}
  178. >
  179. <div className='justify-content-end'>
  180. <SelectStatus
  181. selectedStatus={config.availableStatuses.find(s => s.slug === content.status)}
  182. availableStatus={config.availableStatuses}
  183. onChangeStatus={this.handleChangeStatus}
  184. disabled={false}
  185. />
  186. <ArchiveDeleteContent
  187. customColor={config.hexcolor}
  188. onClickArchiveBtn={this.handleClickArchive}
  189. onClickDeleteBtn={this.handleClickDelete}
  190. disabled={false}
  191. />
  192. </div>
  193. </PopinFixedOption>
  194. <PopinFixedContent
  195. customClass={`${config.slug}__contentpage`}
  196. >
  197. <Timeline
  198. customClass={`${config.slug}__contentpage`}
  199. customColor={config.hexcolor}
  200. loggedUser={loggedUser}
  201. timelineData={listMessage}
  202. newComment={newComment}
  203. disableComment={false}
  204. wysiwyg={timelineWysiwyg}
  205. onChangeNewComment={this.handleChangeNewComment}
  206. onClickValidateNewCommentBtn={this.handleClickValidateNewCommentBtn}
  207. onClickWysiwygBtn={this.handleToggleWysiwyg}
  208. onClickRevisionBtn={() => {}}
  209. shouldScrollToBottom
  210. showHeader={false}
  211. />
  212. </PopinFixedContent>
  213. </PopinFixed>
  214. )
  215. }
  216. }
  217. export default Thread