import React, { useState, useEffect, useRef, useMemo, useCallback, } from 'react'; import type { IRevisionOnConflict } from '@growi/core'; import { UserPicture } from '@growi/ui/dist/components'; import CodeMirror from 'codemirror/lib/codemirror'; import { format, parseISO } from 'date-fns'; import { useTranslation } from 'next-i18next'; import { Modal, ModalHeader, ModalBody, ModalFooter, } from 'reactstrap'; import { useSaveOrUpdate } from '~/client/services/page-operation'; import { toastError, toastSuccess } from '~/client/util/toastr'; import { OptionsToSave } from '~/interfaces/page-operation'; import { useCurrentPathname, useCurrentUser } from '~/stores/context'; import { useCurrentPagePath, useSWRxCurrentPage, useCurrentPageId } from '~/stores/page'; import { useRemoteRevisionBody, useRemoteRevisionId, useRemoteRevisionLastUpdatedAt, useRemoteRevisionLastUpdateUser, useSetRemoteLatestPageData, } from '~/stores/remote-latest-page'; import ExpandOrContractButton from '../ExpandOrContractButton'; import { UncontrolledCodeMirror } from '../UncontrolledCodeMirror'; require('codemirror/lib/codemirror.css'); require('codemirror/addon/merge/merge'); require('codemirror/addon/merge/merge.css'); const DMP = require('diff_match_patch'); Object.keys(DMP).forEach((key) => { window[key] = DMP[key] }); type ConflictDiffModalProps = { isOpen?: boolean; onClose?: (() => void); markdownOnEdit: string; optionsToSave: OptionsToSave | undefined; afterResolvedHandler: () => void, }; type ConflictDiffModalCoreProps = { isOpen?: boolean; onClose?: (() => void); optionsToSave: OptionsToSave | undefined; request: IRevisionOnConflictWithStringDate, origin: IRevisionOnConflictWithStringDate, latest: IRevisionOnConflictWithStringDate, afterResolvedHandler: () => void, }; type IRevisionOnConflictWithStringDate = Omit & { createdAt: string } const ConflictDiffModalCore = (props: ConflictDiffModalCoreProps): JSX.Element => { const { onClose, request, origin, latest, optionsToSave, afterResolvedHandler, } = props; const { t } = useTranslation(''); const [resolvedRevision, setResolvedRevision] = useState(''); const [isRevisionselected, setIsRevisionSelected] = useState(false); const [isModalExpanded, setIsModalExpanded] = useState(false); const [codeMirrorRef, setCodeMirrorRef] = useState(null); const { data: remoteRevisionId } = useRemoteRevisionId(); const { setRemoteLatestPageData } = useSetRemoteLatestPageData(); const { data: pageId } = useCurrentPageId(); const { data: currentPagePath } = useCurrentPagePath(); const { data: currentPathname } = useCurrentPathname(); const saveOrUpdate = useSaveOrUpdate(); const uncontrolledRef = useRef(null); useEffect(() => { if (codeMirrorRef != null) { CodeMirror.MergeView(codeMirrorRef, { value: origin.revisionBody, origLeft: request.revisionBody, origRight: latest.revisionBody, lineNumbers: true, collapseIdentical: true, showDifferences: true, highlightDifferences: true, connect: 'connect', readOnly: true, revertButtons: false, }); } }, [codeMirrorRef, origin.revisionBody, request.revisionBody, latest.revisionBody]); const close = useCallback(() => { if (onClose != null) { onClose(); } }, [onClose]); const onResolveConflict = useCallback(async() => { if (currentPathname == null) { return } // disable button after clicked setIsRevisionSelected(false); const codeMirrorVal = uncontrolledRef.current?.editor.doc.getValue(); try { const { page } = await saveOrUpdate( codeMirrorVal, { pageId, path: currentPagePath || currentPathname, revisionId: remoteRevisionId }, optionsToSave, ); const remotePageData = { remoteRevisionId: page.revision._id, remoteRevisionBody: page.revision.body, remoteRevisionLastUpdateUser: page.lastUpdateUser, remoteRevisionLastUpdatedAt: page.updatedAt, revisionIdHackmdSynced: page.revisionIdHackmdSynced, hasDraftOnHackmd: page.hasDraftOnHackmd, }; setRemoteLatestPageData(remotePageData); afterResolvedHandler(); close(); toastSuccess('Saved successfully'); } catch (error) { toastError(`Error occured: ${error.message}`); } }, [afterResolvedHandler, close, currentPagePath, currentPathname, optionsToSave, pageId, remoteRevisionId, saveOrUpdate, setRemoteLatestPageData]); // TODO: No longer support custom close icon in bootstrap v5 // const resizeAndCloseButtons = useMemo(() => ( //
// setIsModalExpanded(true)} // contractWindow={() => setIsModalExpanded(false)} // /> // //
// ), [isModalExpanded, close]); const isOpen = props.isOpen ?? false; return ( {/* */} error{t('modal_resolve_conflict.resolve_conflict')} { isOpen && (

{t('modal_resolve_conflict.resolve_conflict_message')}

{t('modal_resolve_conflict.requested_revision')}

updated by {request.user.username}

{request.createdAt}

{t('modal_resolve_conflict.origin_revision')}

updated by {origin.user.username}

{origin.createdAt}

{t('modal_resolve_conflict.latest_revision')}

updated by {latest.user.username}

{latest.createdAt}

{ setCodeMirrorRef(el) }}>

{t('modal_resolve_conflict.selected_editable_revision')}

)}
); }; export const ConflictDiffModal = (props: ConflictDiffModalProps): JSX.Element => { const { isOpen, onClose, optionsToSave, afterResolvedHandler, } = props; const { data: currentUser } = useCurrentUser(); // state for current page const { data: currentPage } = useSWRxCurrentPage(); // state for latest page const { data: remoteRevisionId } = useRemoteRevisionId(); const { data: remoteRevisionBody } = useRemoteRevisionBody(); const { data: remoteRevisionLastUpdateUser } = useRemoteRevisionLastUpdateUser(); const { data: remoteRevisionLastUpdatedAt } = useRemoteRevisionLastUpdatedAt(); const currentTime: Date = new Date(); const isRemotePageDataInappropriate = remoteRevisionId == null || remoteRevisionBody == null || remoteRevisionLastUpdateUser == null; if (!isOpen || currentUser == null || currentPage == null || isRemotePageDataInappropriate) { return <>; } const currentPageCreatedAtFixed = typeof currentPage.updatedAt === 'string' ? parseISO(currentPage.updatedAt) : currentPage.updatedAt; const request: IRevisionOnConflictWithStringDate = { revisionId: '', revisionBody: props.markdownOnEdit, createdAt: format(currentTime, 'yyyy/MM/dd HH:mm:ss'), user: currentUser, }; const origin: IRevisionOnConflictWithStringDate = { revisionId: currentPage?.revision._id, revisionBody: currentPage?.revision.body, createdAt: format(currentPageCreatedAtFixed, 'yyyy/MM/dd HH:mm:ss'), user: currentPage?.lastUpdateUser, }; const latest: IRevisionOnConflictWithStringDate = { revisionId: remoteRevisionId, revisionBody: remoteRevisionBody, createdAt: format(new Date(remoteRevisionLastUpdatedAt || currentTime.toString()), 'yyyy/MM/dd HH:mm:ss'), user: remoteRevisionLastUpdateUser, }; const propsForCore = { isOpen, onClose, optionsToSave, request, origin, latest, afterResolvedHandler, }; return ; };