import React from 'react' import HtmlDocumentComponent from '../component/HtmlDocument.jsx' import { translate } from 'react-i18next' import i18n from '../i18n.js' import { addAllResourceI18n, handleFetchResult, generateAvatarFromPublicName, PopinFixed, PopinFixedHeader, PopinFixedOption, PopinFixedContent, Timeline, NewVersionBtn, ArchiveDeleteContent, SelectStatus } from 'tracim_frontend_lib' import { MODE, debug } from '../helper.js' import { getHtmlDocContent, getHtmlDocComment, getHtmlDocRevision, postHtmlDocNewComment, putHtmlDocContent, putHtmlDocStatus, putHtmlDocIsArchived, putHtmlDocIsDeleted, putHtmlDocRestoreArchived, putHtmlDocRestoreDeleted, putHtmlDocRead } from '../action.async.js' class HtmlDocument extends React.Component { constructor (props) { super(props) this.state = { appName: 'html-document', isVisible: true, config: props.data ? props.data.config : debug.config, loggedUser: props.data ? props.data.loggedUser : debug.loggedUser, content: props.data ? props.data.content : debug.content, rawContentBeforeEdit: '', timeline: props.data ? [] : [], // debug.timeline, newComment: '', timelineWysiwyg: false, mode: MODE.VIEW } // i18n has been init, add resources from frontend addAllResourceI18n(i18n, this.state.config.translation) i18n.changeLanguage(this.state.loggedUser.lang) document.addEventListener('appCustomEvent', this.customEventReducer) } customEventReducer = ({ detail: { type, data } }) => { // action: { type: '', data: {} } switch (type) { case 'html-document_showApp': console.log('%c Custom event', 'color: #28a745', type, data) this.setState({isVisible: true}) break case 'html-document_hideApp': console.log('%c Custom event', 'color: #28a745', type, data) this.setState({isVisible: false}) break case 'html-document_reloadContent': console.log('%c Custom event', 'color: #28a745', type, data) this.setState(prev => ({content: {...prev.content, ...data}, isVisible: true})) break case 'allApp_changeLang': console.log('%c Custom event', 'color: #28a745', type, data) this.setState(prev => ({ loggedUser: { ...prev.loggedUser, lang: data } })) i18n.changeLanguage(data) break } } componentDidMount () { console.log('%c did mount', `color: ${this.state.config.hexcolor}`) this.loadContent() } componentDidUpdate (prevProps, prevState) { const { state } = this console.log('%c did update', `color: ${this.state.config.hexcolor}`, prevState, state) if (!prevState.content || !state.content) return if (prevState.content.content_id !== state.content.content_id) this.loadContent() if (state.mode === MODE.EDIT && prevState.mode !== state.mode) { tinymce.remove('#wysiwygNewVersion') wysiwyg('#wysiwygNewVersion', this.handleChangeText) } if (!prevState.timelineWysiwyg && state.timelineWysiwyg) wysiwyg('#wysiwygTimelineComment', this.handleChangeNewComment) else if (prevState.timelineWysiwyg && !state.timelineWysiwyg) tinymce.remove('#wysiwygTimelineComment') } componentWillUnmount () { console.log('%c will Unmount', `color: ${this.state.config.hexcolor}`) document.removeEventListener('appCustomEvent', this.customEventReducer) } loadContent = async () => { const { loggedUser, content, config } = this.state const fetchResultHtmlDocument = getHtmlDocContent(loggedUser, config.apiUrl, content.workspace_id, content.content_id) const fetchResultComment = getHtmlDocComment(loggedUser, config.apiUrl, content.workspace_id, content.content_id) const fetchResultRevision = getHtmlDocRevision(loggedUser, config.apiUrl, content.workspace_id, content.content_id) handleFetchResult(await fetchResultHtmlDocument) .then(resHtmlDocument => this.setState({content: resHtmlDocument.body})) .catch(e => console.log('Error loading content.', e)) Promise.all([ handleFetchResult(await fetchResultComment), handleFetchResult(await fetchResultRevision) ]) .then(([resComment, resRevision]) => { const resCommentWithProperDateAndAvatar = resComment.body.map(c => ({ ...c, created: (new Date(c.created)).toLocaleString(), author: { ...c.author, avatar_url: c.author.avatar_url ? c.author.avatar_url : generateAvatarFromPublicName(c.author.public_name) } })) const revisionWithComment = resRevision.body .map((r, i) => ({ ...r, created: (new Date(r.created)).toLocaleString(), timelineType: 'revision', commentList: r.comment_ids.map(ci => ({ timelineType: 'comment', ...resCommentWithProperDateAndAvatar.find(c => c.content_id === ci) })), number: i + 1 })) .reduce((acc, rev) => [ ...acc, rev, ...rev.commentList.map(comment => ({ ...comment, customClass: '', loggedUser: this.state.config.loggedUser })) ], []) this.setState({ timeline: revisionWithComment, mode: resRevision.body.length === 1 ? MODE.EDIT : MODE.VIEW // first time editing the doc, open in edit mode }) }) .catch(e => { console.log('Error loading Timeline.', e) this.setState({timeline: []}) }) await Promise.all([fetchResultHtmlDocument, fetchResultComment, fetchResultRevision]) putHtmlDocRead(loggedUser, config.apiUrl, content.workspace_id, content.content_id) // mark as read after all requests are finished } handleClickBtnCloseApp = () => { this.setState({ isVisible: false }) GLOBAL_dispatchEvent({type: 'appClosed', data: {}}) // handled by tracim_front::src/container/WorkspaceContent.jsx } handleSaveEditTitle = async newTitle => { const { loggedUser, config, content } = this.state const fetchResultSaveHtmlDoc = putHtmlDocContent(loggedUser, config.apiUrl, content.workspace_id, content.content_id, newTitle, content.raw_content) handleFetchResult(await fetchResultSaveHtmlDoc) .then(resSave => { if (resSave.apiResponse.status === 200) { this.loadContent() GLOBAL_dispatchEvent({ type: 'refreshContentList', data: {} }) } else { console.warn('Error saving html-document. Result:', resSave, 'content:', content, 'config:', config) } }) } handleClickNewVersion = () => this.setState(prev => ({ rawContentBeforeEdit: prev.content.raw_content, mode: MODE.EDIT })) handleCloseNewVersion = () => { tinymce.remove('#wysiwygNewVersion') this.setState(prev => ({ content: { ...prev.content, raw_content: prev.rawContentBeforeEdit }, mode: MODE.VIEW })) } handleSaveHtmlDocument = async () => { const { loggedUser, content, config } = this.state const fetchResultSaveHtmlDoc = putHtmlDocContent(loggedUser, config.apiUrl, content.workspace_id, content.content_id, content.label, content.raw_content) handleFetchResult(await fetchResultSaveHtmlDoc) .then(resSave => { if (resSave.apiResponse.status === 200) { this.handleCloseNewVersion() this.loadContent() } else { console.warn('Error saving html-document. Result:', resSave, 'content:', content, 'config:', config) } }) } handleChangeText = e => { const newText = e.target.value // because SyntheticEvent is pooled (react specificity) this.setState(prev => ({content: {...prev.content, raw_content: newText}})) } handleChangeNewComment = e => { const newComment = e.target.value this.setState({newComment}) } handleClickValidateNewCommentBtn = async () => { const { loggedUser, config, content, newComment } = this.state const fetchResultSaveNewComment = await postHtmlDocNewComment(loggedUser, config.apiUrl, content.workspace_id, content.content_id, newComment) handleFetchResult(await fetchResultSaveNewComment) .then(resSave => { if (resSave.apiResponse.status === 200) { this.setState({newComment: ''}) if (this.state.timelineWysiwyg) tinymce.get('wysiwygTimelineComment').setContent('') this.loadContent() } else { console.warn('Error saving html-document comment. Result:', resSave, 'content:', content, 'config:', config) } }) } handleToggleWysiwyg = () => this.setState(prev => ({timelineWysiwyg: !prev.timelineWysiwyg})) handleChangeStatus = async newStatus => { const { loggedUser, config, content } = this.state const fetchResultSaveEditStatus = putHtmlDocStatus(loggedUser, config.apiUrl, content.workspace_id, content.content_id, newStatus) handleFetchResult(await fetchResultSaveEditStatus) .then(resSave => { if (resSave.status !== 204) { // 204 no content so dont take status from resSave.apiResponse.status console.warn('Error saving html-document comment. Result:', resSave, 'content:', content, 'config:', config) } else { this.loadContent() } }) } handleClickArchive = async () => { const { loggedUser, config, content } = this.state const fetchResultArchive = await putHtmlDocIsArchived(loggedUser, config.apiUrl, content.workspace_id, content.content_id) switch (fetchResultArchive.status) { case 204: this.setState(prev => ({content: {...prev.content, is_archived: true}})); break default: GLOBAL_dispatchEvent({ type: 'addFlashMsg', data: { msg: this.props.t('Error while archiving document'), type: 'warning', delay: undefined } }) } } handleClickDelete = async () => { const { loggedUser, config, content } = this.state const fetchResultArchive = await putHtmlDocIsDeleted(loggedUser, config.apiUrl, content.workspace_id, content.content_id) switch (fetchResultArchive.status) { case 204: this.setState(prev => ({content: {...prev.content, is_deleted: true}})); break default: GLOBAL_dispatchEvent({ type: 'addFlashMsg', data: { msg: this.props.t('Error while deleting document'), type: 'warning', delay: undefined } }) } } handleClickRestoreArchived = async () => { const { loggedUser, config, content } = this.state const fetchResultRestore = await putHtmlDocRestoreArchived(loggedUser, config.apiUrl, content.workspace_id, content.content_id) switch (fetchResultRestore.status) { case 204: this.setState(prev => ({content: {...prev.content, is_archived: false}})); break default: GLOBAL_dispatchEvent({ type: 'addFlashMsg', data: { msg: this.props.t('Error while restoring document'), type: 'warning', delay: undefined } }) } } handleClickRestoreDeleted = async () => { const { loggedUser, config, content } = this.state const fetchResultRestore = await putHtmlDocRestoreDeleted(loggedUser, config.apiUrl, content.workspace_id, content.content_id) switch (fetchResultRestore.status) { case 204: this.setState(prev => ({content: {...prev.content, is_deleted: false}})); break default: GLOBAL_dispatchEvent({ type: 'addFlashMsg', data: { msg: this.props.t('Error while restoring document'), type: 'warning', delay: undefined } }) } } handleClickShowRevision = revision => { const { mode, timeline } = this.state const revisionArray = timeline.filter(t => t.timelineType === 'revision') const isLastRevision = revision.revision_id === revisionArray[revisionArray.length - 1].revision_id if (mode === MODE.REVISION && isLastRevision) { this.handleClickLastVersion() return } if (mode === MODE.VIEW && isLastRevision) return this.setState(prev => ({ content: { ...prev.content, label: revision.label, raw_content: revision.raw_content, number: revision.number, status: revision.status, is_archived: prev.is_archived, // archived and delete should always be taken from last version is_deleted: prev.is_deleted }, mode: MODE.REVISION })) } handleClickLastVersion = () => { this.loadContent() this.setState({mode: MODE.VIEW}) } render () { const { isVisible, loggedUser, content, timeline, newComment, timelineWysiwyg, config, mode } = this.state const { t } = this.props if (!isVisible) return null return (
{loggedUser.idRoleUserWorkspace >= 2 && } {mode === MODE.REVISION && }
{loggedUser.idRoleUserWorkspace >= 2 && s.slug === content.status)} availableStatus={config.availableStatuses} onChangeStatus={this.handleChangeStatus} disabled={mode === MODE.REVISION} /> } {loggedUser.idRoleUserWorkspace >= 4 && }
t.timelineType === 'revision').length} text={content.raw_content} onChangeText={this.handleChangeText} isArchived={content.is_archived} isDeleted={content.is_deleted} onClickRestoreArchived={this.handleClickRestoreArchived} onClickRestoreDeleted={this.handleClickRestoreDeleted} key={'html-document'} />
) } } export default translate()(HtmlDocument)