import React from 'react'; import PropTypes from 'prop-types'; import { useTranslation } from 'react-i18next'; import { isEnabledShowUnsavedWarning } from '~/client/util/editor'; import AppContainer from '~/client/services/AppContainer'; import EditorContainer from '~/client/services/EditorContainer'; import PageContainer from '~/client/services/PageContainer'; import { apiPost } from '~/client/util/apiv1-client'; import { getOptionsToSave } from '~/client/util/editor'; import { useCurrentPagePath, useCurrentPageId } from '~/stores/context'; import { useSWRxSlackChannels, useIsSlackEnabled, usePageTagsForEditors } from '~/stores/editor'; import { useEditorMode, useSelectedGrant, useSelectedGrantGroupId, useSelectedGrantGroupName, } from '~/stores/ui'; import loggerFactory from '~/utils/logger'; import HackmdEditor from './PageEditorByHackmd/HackmdEditor'; import { withUnstatedContainers } from './UnstatedUtils'; 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 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 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 { isSlackEnabled, slackChannels, pageContainer, editorContainer, grant, grantGroupId, grantGroupName, pageTags, } = this.props; const optionsToSave = getOptionsToSave(isSlackEnabled, slackChannels, grant, grantGroupId, grantGroupName, pageTags); try { // disable unsaved warning isEnabledShowUnsavedWarning(false) // eslint-disable-next-line no-unused-vars const { page, tags } = await pageContainer.save(markdown, this.props.editorMode, 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(); isEnabledShowUnsavedWarning(true); const params = { pageId: pageContainer.state.pageId, }; try { await 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 */}

) }
); } } PageEditorByHackmd.propTypes = { t: PropTypes.func.isRequired, // i18next appContainer: PropTypes.instanceOf(AppContainer).isRequired, pageContainer: PropTypes.instanceOf(PageContainer).isRequired, editorContainer: PropTypes.instanceOf(EditorContainer).isRequired, // TODO: remove this when omitting unstated is completed editorMode: PropTypes.string.isRequired, isSlackEnabled: PropTypes.bool.isRequired, pageTags: PropTypes.arrayOf(PropTypes.string), slackChannels: PropTypes.string.isRequired, grant: PropTypes.number.isRequired, grantGroupId: PropTypes.string, grantGroupName: PropTypes.string, }; /** * Wrapper component for using unstated */ const PageEditorByHackmdHOCWrapper = withUnstatedContainers(PageEditorByHackmd, [AppContainer, PageContainer, EditorContainer]); const PageEditorByHackmdWrapper = (props) => { const { t } = useTranslation(); const { data: editorMode } = useEditorMode(); const { data: currentPagePath } = useCurrentPagePath(); const { data: slackChannelsData } = useSWRxSlackChannels(currentPagePath); const { data: isSlackEnabled } = useIsSlackEnabled(); const { data: pageId } = useCurrentPageId(); const { data: pageTags } = usePageTagsForEditors(pageId); const { data: grant } = useSelectedGrant(); const { data: grantGroupId } = useSelectedGrantGroupId(); const { data: grantGroupName } = useSelectedGrantGroupName(); if (editorMode == null) { return null; } return ( ); }; export default PageEditorByHackmdWrapper;