Explorar o código

Merge branch 'master' into fix/111106-math-page-vrt

yuken %!s(int64=3) %!d(string=hai) anos
pai
achega
1acc3741b9

+ 1 - 1
packages/app/src/components/Admin/App/AppSettingsPageContents.tsx

@@ -73,7 +73,7 @@ const AppSettingsPageContents = (props: Props) => {
           && (
           && (
             <div className="row">
             <div className="row">
               <div className="col-lg-12">
               <div className="col-lg-12">
-                <h2 className="admin-setting-header">{t('V5 Page Migration')}</h2>
+                <h2 className="admin-setting-header" data-testid="v5-page-migration">{t('V5 Page Migration')}</h2>
                 <V5PageMigration />
                 <V5PageMigration />
               </div>
               </div>
             </div>
             </div>

+ 12 - 6
packages/app/src/components/Page/DisplaySwitcher.tsx

@@ -12,7 +12,9 @@ import {
 } from '~/stores/context';
 } from '~/stores/context';
 import { useDescendantsPageListModal } from '~/stores/modal';
 import { useDescendantsPageListModal } from '~/stores/modal';
 import { useCurrentPagePath, useSWRxCurrentPage } from '~/stores/page';
 import { useCurrentPagePath, useSWRxCurrentPage } from '~/stores/page';
-import { useRemoteRevisionId, useRemoteRevisionLastUpdatUser } from '~/stores/remote-latest-page';
+import {
+  useSetRemoteLatestPageData,
+} from '~/stores/remote-latest-page';
 import { EditorMode, useEditorMode } from '~/stores/ui';
 import { EditorMode, useEditorMode } from '~/stores/ui';
 import { useGlobalSocket } from '~/stores/websocket';
 import { useGlobalSocket } from '~/stores/websocket';
 
 
@@ -47,8 +49,7 @@ const PageView = React.memo((): JSX.Element => {
   const { data: isNotFound } = useIsNotFound();
   const { data: isNotFound } = useIsNotFound();
   const { data: currentPage } = useSWRxCurrentPage(shareLinkId ?? undefined);
   const { data: currentPage } = useSWRxCurrentPage(shareLinkId ?? undefined);
   const { open: openDescendantPageListModal } = useDescendantsPageListModal();
   const { open: openDescendantPageListModal } = useDescendantsPageListModal();
-  const { mutate: mutateRemoteRevisionId } = useRemoteRevisionId();
-  const { mutate: mutateRemoteRevisionLastUpdateUser } = useRemoteRevisionLastUpdatUser();
+  const { setRemoteLatestPageData } = useSetRemoteLatestPageData();
 
 
   const isTopPagePath = isTopPage(currentPagePath ?? '');
   const isTopPagePath = isTopPage(currentPagePath ?? '');
   const isUsersHomePagePath = isUsersHomePage(currentPagePath ?? '');
   const isUsersHomePagePath = isUsersHomePage(currentPagePath ?? '');
@@ -58,9 +59,14 @@ const PageView = React.memo((): JSX.Element => {
   const setLatestRemotePageData = useCallback((data) => {
   const setLatestRemotePageData = useCallback((data) => {
     const { s2cMessagePageUpdated } = data;
     const { s2cMessagePageUpdated } = data;
 
 
-    mutateRemoteRevisionId(s2cMessagePageUpdated.revisionId);
-    mutateRemoteRevisionLastUpdateUser(s2cMessagePageUpdated.remoteLastUpdateUser);
-  }, [mutateRemoteRevisionId, mutateRemoteRevisionLastUpdateUser]);
+    const remoteData = {
+      remoteRevisionId: s2cMessagePageUpdated.revisionId,
+      remoteRevisionBody: s2cMessagePageUpdated.revisionBody,
+      remoteRevisionLastUpdateUser: s2cMessagePageUpdated.remoteLastUpdateUser,
+      remoteRevisionLastUpdatedAt: s2cMessagePageUpdated.revisionUpdateAt,
+    };
+    setRemoteLatestPageData(remoteData);
+  }, [setRemoteLatestPageData]);
 
 
   // listen socket for someone updating this page
   // listen socket for someone updating this page
   useEffect(() => {
   useEffect(() => {

+ 2 - 1
packages/app/src/components/PageAlert/TrashPageAlert.tsx

@@ -73,6 +73,7 @@ export const TrashPageAlert = (): JSX.Element => {
           className="btn btn-info rounded-pill btn-sm ml-auto mr-2"
           className="btn btn-info rounded-pill btn-sm ml-auto mr-2"
           onClick={openPutbackPageModalHandler}
           onClick={openPutbackPageModalHandler}
           data-toggle="modal"
           data-toggle="modal"
+          data-testid="put-back-button"
         >
         >
           <i className="icon-action-undo" aria-hidden="true"></i> { t('Put Back') }
           <i className="icon-action-undo" aria-hidden="true"></i> { t('Put Back') }
         </button>
         </button>
@@ -94,7 +95,7 @@ export const TrashPageAlert = (): JSX.Element => {
 
 
   return (
   return (
     <>
     <>
-      <div className="alert alert-warning py-3 pl-4 d-flex flex-column flex-lg-row">
+      <div className="alert alert-warning py-3 pl-4 d-flex flex-column flex-lg-row" data-testid="trash-page-alert">
         <div className="flex-grow-1">
         <div className="flex-grow-1">
           This page is in the trash <i className="icon-trash" aria-hidden="true"></i>.
           This page is in the trash <i className="icon-trash" aria-hidden="true"></i>.
           <br />
           <br />

+ 1 - 0
packages/app/src/components/PageDeleteModal.tsx

@@ -272,6 +272,7 @@ const PageDeleteModal: FC = () => {
           className={`btn btn-${deleteIconAndKey[deleteMode].color}`}
           className={`btn btn-${deleteIconAndKey[deleteMode].color}`}
           disabled={!isDeletable}
           disabled={!isDeletable}
           onClick={deleteButtonHandler}
           onClick={deleteButtonHandler}
+          data-testid="delete-page-button"
         >
         >
           <i className={`mr-1 icon-${deleteIconAndKey[deleteMode].icon}`} aria-hidden="true"></i>
           <i className={`mr-1 icon-${deleteIconAndKey[deleteMode].icon}`} aria-hidden="true"></i>
           { t(`modal_delete.delete_${deleteIconAndKey[deleteMode].translationKey}`) }
           { t(`modal_delete.delete_${deleteIconAndKey[deleteMode].translationKey}`) }

+ 31 - 10
packages/app/src/components/PageEditor.tsx

@@ -29,7 +29,8 @@ import {
   useIsConflict,
   useIsConflict,
   useEditingMarkdown,
   useEditingMarkdown,
 } from '~/stores/editor';
 } from '~/stores/editor';
-import { useCurrentPagePath, useSWRxCurrentPage } from '~/stores/page';
+import { useConflictDiffModal } from '~/stores/modal';
+import { useCurrentPagePath, useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
 import { usePreviewOptions } from '~/stores/renderer';
 import { usePreviewOptions } from '~/stores/renderer';
 import {
 import {
   EditorMode,
   EditorMode,
@@ -41,6 +42,7 @@ import loggerFactory from '~/utils/logger';
 
 
 
 
 // import { ConflictDiffModal } from './PageEditor/ConflictDiffModal';
 // import { ConflictDiffModal } from './PageEditor/ConflictDiffModal';
+import { ConflictDiffModal } from './PageEditor/ConflictDiffModal';
 import Editor from './PageEditor/Editor';
 import Editor from './PageEditor/Editor';
 import Preview from './PageEditor/Preview';
 import Preview from './PageEditor/Preview';
 import scrollSyncHelper from './PageEditor/ScrollSyncHelper';
 import scrollSyncHelper from './PageEditor/ScrollSyncHelper';
@@ -71,8 +73,9 @@ const PageEditor = React.memo((): JSX.Element => {
   const { data: currentPathname } = useCurrentPathname();
   const { data: currentPathname } = useCurrentPathname();
   const { data: currentPage, mutate: mutateCurrentPage } = useSWRxCurrentPage();
   const { data: currentPage, mutate: mutateCurrentPage } = useSWRxCurrentPage();
   const { data: grantData, mutate: mutateGrant } = useSelectedGrant();
   const { data: grantData, mutate: mutateGrant } = useSelectedGrant();
-  const { data: pageTags } = usePageTagsForEditors(pageId);
-  const { data: editingMarkdown } = useEditingMarkdown();
+  const { data: pageTags, sync: syncTagsInfoForEditor } = usePageTagsForEditors(pageId);
+  const { mutate: mutateTagsInfo } = useSWRxTagsInfo(pageId);
+  const { data: editingMarkdown, mutate: mutateEditingMarkdown } = useEditingMarkdown();
   const { data: isEnabledAttachTitleHeader } = useIsEnabledAttachTitleHeader();
   const { data: isEnabledAttachTitleHeader } = useIsEnabledAttachTitleHeader();
   const { data: templateBodyData } = useTemplateBodyData();
   const { data: templateBodyData } = useTemplateBodyData();
   const { data: isEditable } = useIsEditable();
   const { data: isEditable } = useIsEditable();
@@ -84,6 +87,7 @@ const PageEditor = React.memo((): JSX.Element => {
   const { data: currentIndentSize, mutate: mutateCurrentIndentSize } = useCurrentIndentSize();
   const { data: currentIndentSize, mutate: mutateCurrentIndentSize } = useCurrentIndentSize();
   const { data: isUploadableFile } = useIsUploadableFile();
   const { data: isUploadableFile } = useIsUploadableFile();
   const { data: isUploadableImage } = useIsUploadableImage();
   const { data: isUploadableImage } = useIsUploadableImage();
+  const { data: conflictDiffModalStatus, close: closeConflictDiffModal } = useConflictDiffModal();
 
 
   const { data: rendererOptions, mutate: mutateRendererOptions } = usePreviewOptions();
   const { data: rendererOptions, mutate: mutateRendererOptions } = usePreviewOptions();
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
@@ -411,6 +415,23 @@ const PageEditor = React.memo((): JSX.Element => {
   }, []);
   }, []);
   const scrollEditorByPreviewScrollWithThrottle = useMemo(() => throttle(20, scrollEditorByPreviewScroll), [scrollEditorByPreviewScroll]);
   const scrollEditorByPreviewScrollWithThrottle = useMemo(() => throttle(20, scrollEditorByPreviewScroll), [scrollEditorByPreviewScroll]);
 
 
+  const afterResolvedHandler = useCallback(async() => {
+    // get page data from db
+    const pageData = await mutateCurrentPage();
+
+    // update tag
+    await mutateTagsInfo(); // get from DB
+    syncTagsInfoForEditor(); // sync global state for client
+
+    // clear isConflict
+    mutateIsConflict(false);
+
+    // set resolved markdown in editing markdown
+    const markdown = pageData?.revision.body ?? '';
+    mutateEditingMarkdown(markdown);
+
+  }, [mutateCurrentPage, mutateEditingMarkdown, mutateIsConflict, mutateTagsInfo, syncTagsInfoForEditor]);
+
 
 
   // initialize
   // initialize
   useEffect(() => {
   useEffect(() => {
@@ -514,13 +535,13 @@ const PageEditor = React.memo((): JSX.Element => {
           onScroll={offset => scrollEditorByPreviewScrollWithThrottle(offset)}
           onScroll={offset => scrollEditorByPreviewScrollWithThrottle(offset)}
         />
         />
       </div>
       </div>
-      {/* <ConflictDiffModal
-        isOpen={pageContainer.state.isConflictDiffModalOpen}
-        onClose={() => pageContainer.setState({ isConflictDiffModalOpen: false })}
-        pageContainer={pageContainer}
-        markdownOnEdit={markdown}
-        optionsToSave={optionsToSave}
-      /> */}
+      <ConflictDiffModal
+        isOpen={conflictDiffModalStatus?.isOpened}
+        onClose={() => closeConflictDiffModal()}
+        markdownOnEdit={markdownToPreview}
+        optionsToSave={undefined} // replace undefined
+        afterResolvedHandler={afterResolvedHandler}
+      />
     </div>
     </div>
   );
   );
 });
 });

+ 96 - 45
packages/app/src/components/PageEditor/ConflictDiffModal.tsx

@@ -2,7 +2,6 @@ import React, {
   useState, useEffect, useRef, useMemo, useCallback,
   useState, useEffect, useRef, useMemo, useCallback,
 } from 'react';
 } from 'react';
 
 
-import type { IUser } from '@growi/core';
 import { UserPicture } from '@growi/ui';
 import { UserPicture } from '@growi/ui';
 import CodeMirror from 'codemirror/lib/codemirror';
 import CodeMirror from 'codemirror/lib/codemirror';
 import { format } from 'date-fns';
 import { format } from 'date-fns';
@@ -11,9 +10,14 @@ import {
   Modal, ModalHeader, ModalBody, ModalFooter,
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
+import { useSaveOrUpdate } from '~/client/services/page-operation';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import { OptionsToSave } from '~/interfaces/page-operation';
 import { OptionsToSave } from '~/interfaces/page-operation';
-import { useCurrentUser } from '~/stores/context';
-import { useEditorMode } from '~/stores/ui';
+import { useCurrentPageId, useCurrentPathname, useCurrentUser } from '~/stores/context';
+import { useCurrentPagePath, useSWRxCurrentPage } from '~/stores/page';
+import {
+  useRemoteRevisionBody, useRemoteRevisionId, useRemoteRevisionLastUpdatedAt, useRemoteRevisionLastUpdateUser, useSetRemoteLatestPageData,
+} from '~/stores/remote-latest-page';
 
 
 import { IRevisionOnConflict } from '../../interfaces/revision';
 import { IRevisionOnConflict } from '../../interfaces/revision';
 import ExpandOrContractButton from '../ExpandOrContractButton';
 import ExpandOrContractButton from '../ExpandOrContractButton';
@@ -29,19 +33,29 @@ Object.keys(DMP).forEach((key) => { window[key] = DMP[key] });
 type ConflictDiffModalProps = {
 type ConflictDiffModalProps = {
   isOpen?: boolean;
   isOpen?: boolean;
   onClose?: (() => void);
   onClose?: (() => void);
-  // pageContainer: PageContainer;
   markdownOnEdit: string;
   markdownOnEdit: string;
   optionsToSave: OptionsToSave | undefined;
   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<IRevisionOnConflict, 'createdAt'> & {
 type IRevisionOnConflictWithStringDate = Omit<IRevisionOnConflict, 'createdAt'> & {
   createdAt: string
   createdAt: string
 }
 }
 
 
-const ConflictDiffModalCore = (props: ConflictDiffModalProps & { currentUser: IUser }): JSX.Element => {
-  const { currentUser, onClose } = props;
-
-  const { data: editorMode } = useEditorMode();
+const ConflictDiffModalCore = (props: ConflictDiffModalCoreProps): JSX.Element => {
+  const {
+    onClose, request, origin, latest, optionsToSave, afterResolvedHandler,
+  } = props;
 
 
   const { t } = useTranslation('');
   const { t } = useTranslation('');
   const [resolvedRevision, setResolvedRevision] = useState<string>('');
   const [resolvedRevision, setResolvedRevision] = useState<string>('');
@@ -49,37 +63,15 @@ const ConflictDiffModalCore = (props: ConflictDiffModalProps & { currentUser: IU
   const [isModalExpanded, setIsModalExpanded] = useState<boolean>(false);
   const [isModalExpanded, setIsModalExpanded] = useState<boolean>(false);
   const [codeMirrorRef, setCodeMirrorRef] = useState<HTMLDivElement | null>(null);
   const [codeMirrorRef, setCodeMirrorRef] = useState<HTMLDivElement | null>(null);
 
 
-  const uncontrolledRef = useRef<CodeMirror>(null);
+  const { data: remoteRevisionId } = useRemoteRevisionId();
+  const { setRemoteLatestPageData } = useSetRemoteLatestPageData();
+  const { data: pageId } = useCurrentPageId();
+  const { data: currentPagePath } = useCurrentPagePath();
+  const { data: currentPathname } = useCurrentPathname();
 
 
-  const currentTime: Date = new Date();
+  const saveOrUpdate = useSaveOrUpdate();
 
 
-  const request: IRevisionOnConflictWithStringDate = {
-    revisionId: '',
-    revisionBody: props.markdownOnEdit,
-    createdAt: format(currentTime, 'yyyy/MM/dd HH:mm:ss'),
-    user: currentUser,
-  };
-  const origin: IRevisionOnConflictWithStringDate = {
-    // revisionId: pageContainer.state.revisionId || '',
-    // revisionBody: pageContainer.state.markdown || '',
-    // createdAt: pageContainer.state.updatedAt || '',
-    // user: pageContainer.state.revisionAuthor,
-    revisionId:  '',
-    revisionBody: '',
-    createdAt: '',
-    user: {} as IUser,
-  };
-  const latest: IRevisionOnConflictWithStringDate = {
-    // revisionId: pageContainer.state.remoteRevisionId || '',
-    // revisionBody: pageContainer.state.remoteRevisionBody || '',
-    // createdAt: format(new Date(pageContainer.state.remoteRevisionUpdateAt || currentTime.toString()), 'yyyy/MM/dd HH:mm:ss'),
-    // user: pageContainer.state.lastUpdateUser,
-    revisionId: '',
-    revisionBody: '',
-    createdAt: format(new Date(''), 'yyyy/MM/dd HH:mm:ss'),
-    user: {} as IUser,
-
-  };
+  const uncontrolledRef = useRef<CodeMirror>(null);
 
 
   useEffect(() => {
   useEffect(() => {
     if (codeMirrorRef != null) {
     if (codeMirrorRef != null) {
@@ -105,21 +97,36 @@ const ConflictDiffModalCore = (props: ConflictDiffModalProps & { currentUser: IU
   }, [onClose]);
   }, [onClose]);
 
 
   const onResolveConflict = useCallback(async() => {
   const onResolveConflict = useCallback(async() => {
+    if (currentPathname == null) { return }
     // disable button after clicked
     // disable button after clicked
     setIsRevisionSelected(false);
     setIsRevisionSelected(false);
 
 
     const codeMirrorVal = uncontrolledRef.current?.editor.doc.getValue();
     const codeMirrorVal = uncontrolledRef.current?.editor.doc.getValue();
 
 
     try {
     try {
-      // await pageContainer.resolveConflict(codeMirrorVal, editorMode, props.optionsToSave);
-      // close();
-      // pageContainer.showSuccessToastr();
+      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,
+      };
+      setRemoteLatestPageData(remotePageData);
+      afterResolvedHandler();
+
+      close();
+
+      toastSuccess('Saved successfully');
     }
     }
     catch (error) {
     catch (error) {
-      // pageContainer.showErrorToastr(error);
+      toastError(`Error occured: ${error.message}`);
     }
     }
 
 
-  }, []);
+  }, [afterResolvedHandler, close, currentPagePath, currentPathname, optionsToSave, pageId, remoteRevisionId, saveOrUpdate, setRemoteLatestPageData]);
 
 
   const resizeAndCloseButtons = useMemo(() => (
   const resizeAndCloseButtons = useMemo(() => (
     <div className="d-flex flex-nowrap">
     <div className="d-flex flex-nowrap">
@@ -274,12 +281,56 @@ const ConflictDiffModalCore = (props: ConflictDiffModalProps & { currentUser: IU
 
 
 
 
 export const ConflictDiffModal = (props: ConflictDiffModalProps): JSX.Element => {
 export const ConflictDiffModal = (props: ConflictDiffModalProps): JSX.Element => {
-  const { isOpen } = props;
+  const {
+    isOpen, onClose, optionsToSave, afterResolvedHandler,
+  } = props;
   const { data: currentUser } = useCurrentUser();
   const { data: currentUser } = useCurrentUser();
 
 
-  if (!isOpen || currentUser == null) {
+  // 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 <></>;
     return <></>;
   }
   }
 
 
-  return <ConflictDiffModalCore {...props} currentUser={currentUser} />;
+  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(currentPage.updatedAt, '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 <ConflictDiffModalCore {...propsForCore}/>;
 };
 };

+ 6 - 6
packages/app/src/components/PageStatusAlert.tsx

@@ -7,8 +7,9 @@ import { useEditingMarkdown, useIsConflict } from '~/stores/editor';
 import {
 import {
   useHasDraftOnHackmd, useIsHackmdDraftUpdatingInRealtime, useRevisionIdHackmdSynced,
   useHasDraftOnHackmd, useIsHackmdDraftUpdatingInRealtime, useRevisionIdHackmdSynced,
 } from '~/stores/hackmd';
 } from '~/stores/hackmd';
+import { useConflictDiffModal } from '~/stores/modal';
 import { useSWRxCurrentPage } from '~/stores/page';
 import { useSWRxCurrentPage } from '~/stores/page';
-import { useRemoteRevisionId, useRemoteRevisionLastUpdatUser } from '~/stores/remote-latest-page';
+import { useRemoteRevisionId, useRemoteRevisionLastUpdateUser } from '~/stores/remote-latest-page';
 
 
 import { Username } from './User/Username';
 import { Username } from './User/Username';
 
 
@@ -27,11 +28,12 @@ export const PageStatusAlert = (): JSX.Element => {
   const { data: hasDraftOnHackmd } = useHasDraftOnHackmd();
   const { data: hasDraftOnHackmd } = useHasDraftOnHackmd();
   const { data: isConflict } = useIsConflict();
   const { data: isConflict } = useIsConflict();
   const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
   const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
+  const { open: openConflictDiffModal } = useConflictDiffModal();
 
 
   // store remote latest page data
   // store remote latest page data
   const { data: revisionIdHackmdSynced } = useRevisionIdHackmdSynced();
   const { data: revisionIdHackmdSynced } = useRevisionIdHackmdSynced();
   const { data: remoteRevisionId } = useRemoteRevisionId();
   const { data: remoteRevisionId } = useRemoteRevisionId();
-  const { data: remoteRevisionLastUpdateUser } = useRemoteRevisionLastUpdatUser();
+  const { data: remoteRevisionLastUpdateUser } = useRemoteRevisionLastUpdateUser();
 
 
   const { data: pageData, mutate: mutatePageData } = useSWRxCurrentPage();
   const { data: pageData, mutate: mutatePageData } = useSWRxCurrentPage();
   const revision = pageData?.revision;
   const revision = pageData?.revision;
@@ -42,10 +44,8 @@ export const PageStatusAlert = (): JSX.Element => {
   }, [mutateEditingMarkdown, mutatePageData]);
   }, [mutateEditingMarkdown, mutatePageData]);
 
 
   const onClickResolveConflict = useCallback(() => {
   const onClickResolveConflict = useCallback(() => {
-    // this.props.pageContainer.setState({
-    //   isConflictDiffModalOpen: true,
-    // });
-  }, []);
+    openConflictDiffModal();
+  }, [openConflictDiffModal]);
 
 
   const getContentsForSomeoneEditingAlert = useCallback((): AlertComponentContents => {
   const getContentsForSomeoneEditingAlert = useCallback((): AlertComponentContents => {
     return {
     return {

+ 2 - 2
packages/app/src/components/PutbackPageModal.jsx

@@ -103,7 +103,7 @@ const PutBackPageModal = () => {
     return (
     return (
       <>
       <>
         <ApiErrorMessageList errs={errs} targetPath={targetPath} />
         <ApiErrorMessageList errs={errs} targetPath={targetPath} />
-        <button type="button" className="btn btn-info" onClick={putbackPageButtonHandler}>
+        <button type="button" className="btn btn-info" onClick={putbackPageButtonHandler} data-testid="put-back-execution-button">
           <i className="icon-action-undo mr-2" aria-hidden="true"></i> { t('Put Back') }
           <i className="icon-action-undo mr-2" aria-hidden="true"></i> { t('Put Back') }
         </button>
         </button>
       </>
       </>
@@ -116,7 +116,7 @@ const PutBackPageModal = () => {
   }, [closePutBackPageModal]);
   }, [closePutBackPageModal]);
 
 
   return (
   return (
-    <Modal isOpen={isOpened} toggle={closeModalHandler}>
+    <Modal isOpen={isOpened} toggle={closeModalHandler} data-testid="put-back-page-modal">
       <ModalHeader tag="h4" toggle={closeModalHandler} className="bg-info text-light">
       <ModalHeader tag="h4" toggle={closeModalHandler} className="bg-info text-light">
         <HeaderContent/>
         <HeaderContent/>
       </ModalHeader>
       </ModalHeader>

+ 27 - 0
packages/app/src/stores/modal.tsx

@@ -553,3 +553,30 @@ export const useHandsontableModal = (status?: HandsontableModalStatus): SWRRespo
     close,
     close,
   };
   };
 };
 };
+
+/*
+ * ConflictDiffModal
+ */
+type ConflictDiffModalStatus = {
+  isOpened: boolean,
+}
+
+type ConflictDiffModalUtils = {
+  open(): void,
+  close(): void,
+}
+
+export const useConflictDiffModal = (): SWRResponse<ConflictDiffModalStatus, Error> & ConflictDiffModalUtils => {
+
+  const initialStatus: ConflictDiffModalStatus = { isOpened: false };
+  const swrResponse = useStaticSWR<ConflictDiffModalStatus, Error>('conflictDiffModal', undefined, { fallbackData: initialStatus });
+
+  return Object.assign(swrResponse, {
+    open: () => {
+      swrResponse.mutate({ isOpened: true });
+    },
+    close: () => {
+      swrResponse.mutate({ isOpened: false });
+    },
+  });
+};

+ 38 - 3
packages/app/src/stores/remote-latest-page.ts

@@ -10,9 +10,44 @@ export const useRemoteRevisionId = (initialData?: string): SWRResponse<string, E
 };
 };
 
 
 export const useRemoteRevisionBody = (initialData?: string): SWRResponse<string, Error> => {
 export const useRemoteRevisionBody = (initialData?: string): SWRResponse<string, Error> => {
-  return useStaticSWR<string, Error>('remoteRevisionId', initialData);
+  return useStaticSWR<string, Error>('remoteRevisionBody', initialData);
+};
+
+export const useRemoteRevisionLastUpdateUser = (initialData?: IUser): SWRResponse<IUser, Error> => {
+  return useStaticSWR<IUser, Error>('remoteRevisionLastUpdateUser', initialData);
+};
+
+export const useRemoteRevisionLastUpdatedAt = (initialData?: Date): SWRResponse<Date, Error> => {
+  return useStaticSWR<Date, Error>('remoteRevisionLastUpdatedAt', initialData);
 };
 };
 
 
-export const useRemoteRevisionLastUpdatUser = (initialData?: IUser): SWRResponse<IUser, Error> => {
-  return useStaticSWR<IUser, Error>('remoteRevisionLastUpdatUser', initialData);
+type RemoteRevisionData = {
+  remoteRevisionId: string,
+  remoteRevisionBody: string,
+  remoteRevisionLastUpdateUser: IUser,
+  remoteRevisionLastUpdatedAt: Date
+}
+
+
+// set remote data all at once
+export const useSetRemoteLatestPageData = (): { setRemoteLatestPageData: (pageData: RemoteRevisionData) => void } => {
+  const { mutate: mutateRemoteRevisionId } = useRemoteRevisionId();
+  const { mutate: mutateRemoteRevisionBody } = useRemoteRevisionBody();
+  const { mutate: mutateRemoteRevisionLastUpdateUser } = useRemoteRevisionLastUpdateUser();
+  const { mutate: mutateRemoteRevisionLastUpdatedAt } = useRemoteRevisionLastUpdatedAt();
+
+  const setRemoteLatestPageData = (remoteRevisionData: RemoteRevisionData) => {
+    const {
+      remoteRevisionId, remoteRevisionBody, remoteRevisionLastUpdateUser, remoteRevisionLastUpdatedAt,
+    } = remoteRevisionData;
+    mutateRemoteRevisionId(remoteRevisionId);
+    mutateRemoteRevisionBody(remoteRevisionBody);
+    mutateRemoteRevisionLastUpdateUser(remoteRevisionLastUpdateUser);
+    mutateRemoteRevisionLastUpdatedAt(remoteRevisionLastUpdatedAt);
+  };
+
+  return {
+    setRemoteLatestPageData,
+  };
+
 };
 };

+ 6 - 0
packages/app/src/styles/bootstrap/_variables.scss

@@ -36,6 +36,12 @@ $red: #ff0a54 !default;
 
 
 $enable-shadows: true;
 $enable-shadows: true;
 
 
+// Links
+//
+// Style anchor elements.
+
+$link-hover-decoration: none !default;
+
 // Grid breakpoints
 // Grid breakpoints
 //
 //
 // Define the minimum dimensions at which your layout will change,
 // Define the minimum dimensions at which your layout will change,

+ 2 - 2
packages/app/src/styles/theme/_apply-colors.scss

@@ -553,11 +553,11 @@ body.editing-sidebar {
 }
 }
 
 
 .grid-preview-col-1 {
 .grid-preview-col-1 {
-  background: $info;
+  background: var(--info);
 }
 }
 
 
 .grid-preview-col-2 {
 .grid-preview-col-2 {
-  background: $success;
+  background: var(--success);
 }
 }
 
 
 .grid-preview-col-3 {
 .grid-preview-col-3 {

+ 1 - 1
packages/app/src/styles/theme/_reboot-bootstrap-border-colors.scss

@@ -25,5 +25,5 @@
 }
 }
 
 
 .border-info {
 .border-info {
-  border-color: $info !important;
+  border-color: ver(--info) !important;
 }
 }

+ 4 - 4
packages/app/src/styles/theme/_reboot-toastr-colors.scss

@@ -1,15 +1,15 @@
 .toast-success {
 .toast-success {
-  background-color: $success;
+  background-color: var(--success);
 }
 }
 
 
 .toast-error {
 .toast-error {
-  background-color: $danger;
+  background-color: var(--danger);
 }
 }
 
 
 .toast-info {
 .toast-info {
-  background-color: $info;
+  background-color: var(--info);
 }
 }
 
 
 .toast-warning {
 .toast-warning {
-  background-color: $warning;
+  background-color: var(--warning);
 }
 }

+ 17 - 5
packages/app/test/cypress/integration/20-basic-features/20-basic-features--use-tools.spec.ts

@@ -132,16 +132,28 @@ context('Modal for page operation', () => {
     cy.screenshot(`${ssPrefix}create-template-for-descendants-error`);
     cy.screenshot(`${ssPrefix}create-template-for-descendants-error`);
   });
   });
 
 
-  it('PageDeleteModal is shown successfully', () => {
+  it('Page Deletion and PutBack is executed successfully', () => {
     cy.visit('/Sandbox/Bootstrap4');
     cy.visit('/Sandbox/Bootstrap4');
     cy.waitUntilSkeletonDisappear();
     cy.waitUntilSkeletonDisappear();
 
 
-     cy.get('#grw-subnav-container').within(() => {
-       cy.getByTestid('open-page-item-control-btn').click({force: true});
-       cy.getByTestid('open-page-delete-modal-btn').click({force: true});
+    cy.get('#grw-subnav-container').within(() => {
+      cy.getByTestid('open-page-item-control-btn').click({force: true});
+      cy.getByTestid('open-page-delete-modal-btn').click({force: true});
     });
     });
 
 
-     cy.getByTestid('page-delete-modal').should('be.visible').screenshot(`${ssPrefix}-delete-bootstrap4`);
+    cy.getByTestid('page-delete-modal').should('be.visible').within(() => {
+      cy.screenshot(`${ssPrefix}-delete-modal`);
+      cy.getByTestid('delete-page-button').click();
+    });
+    cy.getByTestid('trash-page-alert').should('be.visible');
+    cy.screenshot(`${ssPrefix}-bootstrap4-is-in-garbage-box`);
+
+    cy.getByTestid('put-back-button').click();
+    cy.getByTestid('put-back-page-modal').should('be.visible').within(() => {
+      cy.screenshot(`${ssPrefix}-put-back-modal`);
+      cy.getByTestid('put-back-execution-button').should('be.visible').click();
+    });
+    cy.screenshot(`${ssPrefix}-put-backed-bootstrap4-page`);
   });
   });
 
 
   it('PageDuplicateModal is shown successfully', () => {
   it('PageDuplicateModal is shown successfully', () => {

+ 2 - 0
packages/app/test/cypress/integration/40-admin/40-admin--access-to-admin-page.spec.ts

@@ -32,6 +32,8 @@ context('Access to Admin page', () => {
   it('/admin/app is successfully loaded', () => {
   it('/admin/app is successfully loaded', () => {
     cy.visit('/admin/app');
     cy.visit('/admin/app');
     cy.getByTestid('admin-app-settings').should('be.visible');
     cy.getByTestid('admin-app-settings').should('be.visible');
+    cy.getByTestid('v5-page-migration').should('be.visible');
+    cy.get('#cbFileUpload').should('be.checked')
     cy.screenshot(`${ssPrefix}-admin-app`);
     cy.screenshot(`${ssPrefix}-admin-app`);
   });
   });
 
 

+ 6 - 0
packages/preset-themes/src/styles/bootstrap/_variables.scss

@@ -50,6 +50,12 @@ $grid-breakpoints: (
   xxl: 1480px,
   xxl: 1480px,
 );
 );
 
 
+// Links
+//
+// Style anchor elements.
+
+$link-hover-decoration: none !default;
+
 // Grid containers
 // Grid containers
 //
 //
 // Define the maximum width of `.container` for different screen sizes.
 // Define the maximum width of `.container` for different screen sizes.

+ 4 - 4
packages/preset-themes/src/styles/default.scss

@@ -7,10 +7,10 @@
 
 
 // colors for overriding bootstrap $theme-colors
 // colors for overriding bootstrap $theme-colors
 // $secondary: #;
 // $secondary: #;
-// $info: #;
-// $success: #;
-// $warning: #;
-// $danger: #;
+// --info: #;
+// --success: #;
+// --warning: #;
+// --danger: #;
 // $light: #;
 // $light: #;
 // $dark: #;
 // $dark: #;
 
 

+ 0 - 8
packages/preset-themes/src/styles/theme/_apply-colors.scss

@@ -551,14 +551,6 @@ body.editing-sidebar {
   background: var.$growi-blue;
   background: var.$growi-blue;
 }
 }
 
 
-.grid-preview-col-1 {
-  background: $info;
-}
-
-.grid-preview-col-2 {
-  background: $success;
-}
-
 .grid-preview-col-3 {
 .grid-preview-col-3 {
   background: var.$growi-green;
   background: var.$growi-green;
 }
 }

+ 0 - 4
packages/preset-themes/src/styles/theme/_reboot-bootstrap-border-colors.scss

@@ -23,7 +23,3 @@
 .border-left {
 .border-left {
   border-left: $border-width solid $border-color !important;
   border-left: $border-width solid $border-color !important;
 }
 }
-
-.border-info {
-  border-color: $info !important;
-}

+ 0 - 15
packages/preset-themes/src/styles/theme/_reboot-toastr-colors.scss

@@ -1,15 +0,0 @@
-.toast-success {
-  background-color: $success;
-}
-
-.toast-error {
-  background-color: $danger;
-}
-
-.toast-info {
-  background-color: $info;
-}
-
-.toast-warning {
-  background-color: $warning;
-}