import React from 'react'; import PropTypes from 'prop-types'; import loggerFactory from '@alias/logger'; import { withTranslation } from 'react-i18next'; import AppContainer from '../services/AppContainer'; import PageContainer from '../services/PageContainer'; import EditorContainer from '../services/EditorContainer'; import { withUnstatedContainers } from './UnstatedUtils'; import HackmdEditor from './PageEditorByHackmd/HackmdEditor'; const logger = loggerFactory('growi:PageEditorByHackmd'); class PageEditorByHackmd extends React.Component { constructor(props) { super(props); this.state = { isInitialized: false, isInitializing: false, // for error hasError: false, errorMessage: '', errorReason: '', }; this.getHackmdUri = this.getHackmdUri.bind(this); this.startToEdit = this.startToEdit.bind(this); this.resumeToEdit = this.resumeToEdit.bind(this); this.onSaveWithShortcut = this.onSaveWithShortcut.bind(this); this.hackmdEditorChangeHandler = this.hackmdEditorChangeHandler.bind(this); this.penpalErrorOccuredHandler = this.penpalErrorOccuredHandler.bind(this); } componentWillMount() { this.props.appContainer.registerComponentInstance('PageEditorByHackmd', this); } /** * return markdown document of HackMD * @return {Promise} */ getMarkdown() { const { t } = this.props; if (!this.state.isInitialized) { return Promise.reject(new Error(t('hackmd.not_initialized'))); } return this.hackmdEditor.getValue(); } /** * reset initialized status */ reset() { this.setState({ isInitialized: false }); } getHackmdUri() { const envVars = this.props.appContainer.getConfig().env; return envVars.HACKMD_URI; } get isResume() { const { pageContainer } = this.props; const { pageIdOnHackmd, hasDraftOnHackmd, isHackmdDraftUpdatingInRealtime, } = pageContainer.state; const isPageExistsOnHackmd = (pageIdOnHackmd != null); return (isPageExistsOnHackmd && hasDraftOnHackmd) || isHackmdDraftUpdatingInRealtime; } /** * Start integration with HackMD */ async startToEdit() { const { pageContainer } = this.props; const hackmdUri = this.getHackmdUri(); if (hackmdUri == null) { // do nothing return; } this.setState({ isInitialized: false, isInitializing: true, }); const params = { pageId: pageContainer.state.pageId, }; try { const res = await this.props.appContainer.apiPost('/hackmd.integrate', params); if (!res.ok) { throw new Error(res.error); } await pageContainer.setState({ pageIdOnHackmd: res.pageIdOnHackmd, revisionIdHackmdSynced: res.revisionIdHackmdSynced, }); } catch (err) { pageContainer.showErrorToastr(err); this.setState({ hasError: true, errorMessage: 'GROWI server failed to connect to HackMD.', errorReason: err.toString(), }); } this.setState({ isInitialized: true, isInitializing: false, }); } /** * Start to edit w/o any api request */ resumeToEdit() { this.setState({ isInitialized: true }); } /** * Reset draft */ async discardChanges() { const { pageContainer } = this.props; const { pageId } = pageContainer.state; try { const res = await this.props.appContainer.apiPost('/hackmd.discard', { pageId }); if (!res.ok) { throw new Error(res.error); } this.props.pageContainer.setState({ isHackmdDraftUpdatingInRealtime: false, hasDraftOnHackmd: false, pageIdOnHackmd: res.pageIdOnHackmd, remoteRevisionId: res.revisionIdHackmdSynced, revisionIdHackmdSynced: res.revisionIdHackmdSynced, }); } catch (err) { logger.error(err); pageContainer.showErrorToastr(err); } } /** * save and update state of containers * @param {string} markdown */ async onSaveWithShortcut(markdown) { const { pageContainer, editorContainer } = this.props; const optionsToSave = editorContainer.getCurrentOptionsToSave(); try { // disable unsaved warning editorContainer.disableUnsavedWarning(); // eslint-disable-next-line no-unused-vars const { page, tags } = await pageContainer.save(markdown, optionsToSave); logger.debug('success to save'); pageContainer.showSuccessToastr(); // update state of EditorContainer editorContainer.setState({ tags }); } catch (error) { logger.error('failed to save', error); pageContainer.showErrorToastr(error); } } /** * onChange event of HackmdEditor handler */ async hackmdEditorChangeHandler(body) { const hackmdUri = this.getHackmdUri(); const { pageContainer, editorContainer } = this.props; if (hackmdUri == null) { // do nothing return; } // do nothing if contents are same if (pageContainer.state.markdown === body) { return; } // enable unsaved warning editorContainer.enableUnsavedWarning(); const params = { pageId: pageContainer.state.pageId, }; try { await this.props.appContainer.apiPost('/hackmd.saveOnHackmd', params); } catch (err) { logger.error(err); } } penpalErrorOccuredHandler(error) { const { pageContainer, t } = this.props; pageContainer.showErrorToastr(error); this.setState({ hasError: true, errorMessage: t('hackmd.fail_to_connect'), errorReason: error.toString(), }); } renderPreInitContent() { const hackmdUri = this.getHackmdUri(); const { pageContainer, t } = this.props; const { revisionId, revisionIdHackmdSynced, remoteRevisionId, pageId, } = pageContainer.state; const isPageNotFound = pageId == null; let content; /* * HackMD is not setup */ if (hackmdUri == null) { content = (

{ t('hackmd.not_set_up')}

{/* eslint-disable-next-line react/no-danger */}

); } /* * used HackMD from NotFound Page */ else if (isPageNotFound) { content = (

{ t('hackmd.used_for_not_found') }

{/* eslint-disable-next-line react/no-danger */}

); } /* * Resume to edit or discard changes */ else if (this.isResume) { const isHackmdDocumentOutdated = revisionIdHackmdSynced !== remoteRevisionId; content = (

HackMD is READY!

{t('hackmd.unsaved_draft')}

{ isHackmdDocumentOutdated && (
{t('hackmd.draft_outdated')}
{t('hackmd.based_on_revision')}  {revisionIdHackmdSynced.substr(-8)}
) } { !isHackmdDocumentOutdated && (
) }
); } /* * Start to edit */ else { const isRevisionOutdated = revisionId !== remoteRevisionId; content = (

HackMD is READY!

{t('hackmd.clone_page_content')}

); } return (
{content}
); } render() { const hackmdUri = this.getHackmdUri(); const { pageContainer, t } = this.props; const { markdown, pageIdOnHackmd, } = pageContainer.state; let content; if (this.state.isInitialized) { content = ( { this.hackmdEditor = c }} hackmdUri={hackmdUri} pageIdOnHackmd={pageIdOnHackmd} initializationMarkdown={this.isResume ? null : markdown} onChange={this.hackmdEditorChangeHandler} onSaveWithShortcut={(document) => { this.onSaveWithShortcut(document); }} onPenpalErrorOccured={this.penpalErrorOccuredHandler} > ); } else { content = this.renderPreInitContent(); } return (
{content} { this.state.hasError && (

{t('hackmd.integration_failed')}

{this.state.errorMessage}

{this.state.errorReason}

{/* eslint-disable-next-line react/no-danger */}

) }
); } } /** * Wrapper component for using unstated */ const PageEditorByHackmdWrapper = withUnstatedContainers(PageEditorByHackmd, [AppContainer, PageContainer, EditorContainer]); PageEditorByHackmd.propTypes = { t: PropTypes.func.isRequired, // i18next appContainer: PropTypes.instanceOf(AppContainer).isRequired, pageContainer: PropTypes.instanceOf(PageContainer).isRequired, editorContainer: PropTypes.instanceOf(EditorContainer).isRequired, }; export default withTranslation()(PageEditorByHackmdWrapper);