import React, { useCallback, useEffect, useRef, useState, } from 'react'; import PropTypes from 'prop-types'; import { useTranslation } from 'react-i18next'; 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, } from '~/stores/ui'; import loggerFactory from '~/utils/logger'; import HackmdEditor from './PageEditorByHackmd/HackmdEditor'; import { withUnstatedContainers } from './UnstatedUtils'; const logger = loggerFactory('growi:PageEditorByHackmd'); const PageEditorByHackmd = (props) => { const { appContainer, pageContainer, editorContainer } = props; // wip 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 slackChannels = slackChannelsData.toString(); const [isInitialized, setIsInitialized] = useState(false); const [isInitializing, setIsInitializing] = useState(false); // for error const [hasError, setHasError] = useState(false); const [errorMessage, setErrorMessage] = useState(''); const [errorReason, setErrorReason] = useState(''); const hackmdEditorRef = useRef(null); useEffect(() => { const pageEditorByHackmdInstance = { getMarkdown: () => { if (!isInitialized) { return Promise.reject(new Error(t('hackmd.not_initialized'))); } return hackmdEditorRef.current.getValue(); }, reset: () => { setIsInitialized(false); }, }; appContainer.registerComponentInstance('PageEditorByHackmd', pageEditorByHackmdInstance); }, [appContainer, isInitialized, t]); const getHackmdUri = useCallback(() => { const envVars = appContainer.getConfig().env; return envVars.HACKMD_URI; }, [appContainer]); const isResume = useCallback(() => { const { pageIdOnHackmd, hasDraftOnHackmd, isHackmdDraftUpdatingInRealtime, } = pageContainer.state; const isPageExistsOnHackmd = (pageIdOnHackmd != null); return (isPageExistsOnHackmd && hasDraftOnHackmd) || isHackmdDraftUpdatingInRealtime; }, [pageContainer.state]); const startToEdit = useCallback(async() => { const hackmdUri = getHackmdUri(); if (hackmdUri == null) { // do nothing return; } setIsInitialized(false); setIsInitializing(true); const params = { 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); setHasError(true); setErrorMessage('GROWI server failed to connect to HackMD.'); setErrorReason(err.toString()); } setIsInitialized(true); setIsInitializing(false); }, [getHackmdUri, pageContainer, pageId]); /** * Start to edit w/o any api request */ const resumeToEdit = useCallback(() => { setIsInitialized(true); }, []); const discardChanges = useCallback(async() => { const { pageId } = pageContainer.state; try { const res = await apiPost('/hackmd.discard', { pageId }); if (!res.ok) { throw new Error(res.error); } pageContainer.setState({ isHackmdDraftUpdatingInRealtime: false, hasDraftOnHackmd: false, pageIdOnHackmd: res.pageIdOnHackmd, remoteRevisionId: res.revisionIdHackmdSynced, revisionIdHackmdSynced: res.revisionIdHackmdSynced, }); } catch (err) { logger.error(err); pageContainer.showErrorToastr(err); } }, [pageContainer]); /** * save and update state of containers * @param {string} markdown */ const onSaveWithShortcut = useCallback(async(markdown) => { const optionsToSave = getOptionsToSave(isSlackEnabled, slackChannels, grant, grant.grantedGroup.id, grant.grantedGroup.name, pageTags); try { // disable unsaved warning editorContainer.disableUnsavedWarning(); // eslint-disable-next-line no-unused-vars const { page, tags } = await pageContainer.save(markdown, 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); } }, [editorContainer, editorMode, grant, isSlackEnabled, pageContainer, pageTags, slackChannels]); /** * onChange event of HackmdEditor handler */ const hackmdEditorChangeHandler = useCallback(async(body) => { const hackmdUri = getHackmdUri(); 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 apiPost('/hackmd.saveOnHackmd', params); } catch (err) { logger.error(err); } }, [editorContainer, getHackmdUri, pageContainer.state.markdown, pageContainer.state.pageId]); const penpalErrorOccuredHandler = useCallback((error) => { pageContainer.showErrorToastr(error); setHasError(true); setErrorMessage(t('hackmd.fail_to_connect')); setErrorReason(error.toString()); }, [pageContainer, t]); const renderPreInitContent = useCallback(() => { const hackmdUri = getHackmdUri(); 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 */}{ t('hackmd.used_for_not_found') }
{/* eslint-disable-next-line react/no-danger */}HackMD is READY!
{t('hackmd.unsaved_draft')}
{ isHackmdDocumentOutdated && (HackMD is READY!
{t('hackmd.clone_page_content')}
{errorReason}
{/* eslint-disable-next-line react/no-danger */}