Shun Miyazawa 1 год назад
Родитель
Сommit
dc10c6ca9c

+ 7 - 2
apps/app/src/client/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -7,6 +7,8 @@ import type {
   IPageToRenameWithMeta, IPageWithMeta, IPageInfoForEntity,
   IPageToRenameWithMeta, IPageWithMeta, IPageInfoForEntity,
 } from '@growi/core';
 } from '@growi/core';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { pagePathUtils } from '@growi/core/dist/utils';
+import { GlobalCodeMirrorEditorKey } from '@growi/editor';
+import { useCodeMirrorEditorIsolated } from '@growi/editor/dist/client/stores/codemirror-editor';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import Link from 'next/link';
 import Link from 'next/link';
@@ -77,19 +79,22 @@ const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element
   const { open: openPresentationModal } = usePagePresentationModal();
   const { open: openPresentationModal } = usePagePresentationModal();
   const { open: openAccessoriesModal } = usePageAccessoriesModal();
   const { open: openAccessoriesModal } = usePageAccessoriesModal();
 
 
+  const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.MAIN);
+
   const syncLatestRevisionBodyHandler = useCallback(async() => {
   const syncLatestRevisionBodyHandler = useCallback(async() => {
     // eslint-disable-next-line no-alert
     // eslint-disable-next-line no-alert
     const answer = window.confirm(t('sync-latest-reevision-body.confirm'));
     const answer = window.confirm(t('sync-latest-reevision-body.confirm'));
     if (answer) {
     if (answer) {
       try {
       try {
-        await syncLatestRevisionBody(pageId);
+        const editingMarkdownLength = codeMirrorEditor?.getDoc().length;
+        await syncLatestRevisionBody(pageId, editingMarkdownLength);
         toastSuccess(t('sync-latest-reevision-body.success-toaster'));
         toastSuccess(t('sync-latest-reevision-body.success-toaster'));
       }
       }
       catch {
       catch {
         toastError(t('sync-latest-reevision-body.error-toaster'));
         toastError(t('sync-latest-reevision-body.error-toaster'));
       }
       }
     }
     }
-  }, [pageId, t]);
+  }, [codeMirrorEditor, pageId, t]);
 
 
   return (
   return (
     <>
     <>

+ 2 - 2
apps/app/src/client/services/page-operation.ts

@@ -175,7 +175,7 @@ export const unpublish = async(pageId: string): Promise<IPageHasId> => {
   return res.data;
   return res.data;
 };
 };
 
 
-export const syncLatestRevisionBody = async(pageId: string): Promise<void> => {
-  await apiv3Put(`/page/${pageId}/sync-latest-revision-body-to-yjs-draft`);
+export const syncLatestRevisionBody = async(pageId: string, editingMarkdownLength?: number): Promise<void> => {
+  await apiv3Put(`/page/${pageId}/sync-latest-revision-body-to-yjs-draft`, { editingMarkdownLength });
   return;
   return;
 };
 };

+ 11 - 4
apps/app/src/server/routes/apiv3/page/sync-latest-revision-body-to-yjs-draft.ts

@@ -1,8 +1,10 @@
+import { Console } from 'console';
+
 import type { IPage, IUserHasId } from '@growi/core';
 import type { IPage, IUserHasId } from '@growi/core';
 import { ErrorV3 } from '@growi/core/dist/models';
 import { ErrorV3 } from '@growi/core/dist/models';
 import type { Request, RequestHandler } from 'express';
 import type { Request, RequestHandler } from 'express';
 import type { ValidationChain } from 'express-validator';
 import type { ValidationChain } from 'express-validator';
-import { param } from 'express-validator';
+import { param, body } from 'express-validator';
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 
 
 import type Crowi from '~/server/crowi';
 import type Crowi from '~/server/crowi';
@@ -21,7 +23,10 @@ type SyncLatestRevisionBodyToYjsDraftHandlerFactory = (crowi: Crowi) => RequestH
 type ReqParams = {
 type ReqParams = {
   pageId: string,
   pageId: string,
 }
 }
-interface Req extends Request<ReqParams, ApiV3Response> {
+type ReqBody = {
+  editingMarkdownLength?: number,
+}
+interface Req extends Request<ReqParams, ApiV3Response, ReqBody> {
   user: IUserHasId,
   user: IUserHasId,
 }
 }
 export const syncLatestRevisionBodyToYjsDraftHandlerFactory: SyncLatestRevisionBodyToYjsDraftHandlerFactory = (crowi) => {
 export const syncLatestRevisionBodyToYjsDraftHandlerFactory: SyncLatestRevisionBodyToYjsDraftHandlerFactory = (crowi) => {
@@ -32,6 +37,7 @@ export const syncLatestRevisionBodyToYjsDraftHandlerFactory: SyncLatestRevisionB
   // define validators for req.params
   // define validators for req.params
   const validator: ValidationChain[] = [
   const validator: ValidationChain[] = [
     param('pageId').isMongoId().withMessage('The param "pageId" must be specified'),
     param('pageId').isMongoId().withMessage('The param "pageId" must be specified'),
+    body('editingMarkdownLength').optional().isInt().withMessage('The body "editingMarkdownLength" must be integer'),
   ];
   ];
 
 
   return [
   return [
@@ -39,6 +45,7 @@ export const syncLatestRevisionBodyToYjsDraftHandlerFactory: SyncLatestRevisionB
     validator, apiV3FormValidator,
     validator, apiV3FormValidator,
     async(req: Req, res: ApiV3Response) => {
     async(req: Req, res: ApiV3Response) => {
       const { pageId } = req.params;
       const { pageId } = req.params;
+      const { editingMarkdownLength } = req.body;
 
 
       // check whether accessible
       // check whether accessible
       if (!(await Page.isAccessiblePageByViewer(pageId, req.user))) {
       if (!(await Page.isAccessiblePageByViewer(pageId, req.user))) {
@@ -47,8 +54,8 @@ export const syncLatestRevisionBodyToYjsDraftHandlerFactory: SyncLatestRevisionB
 
 
       try {
       try {
         const yjsService = getYjsService();
         const yjsService = getYjsService();
-        await yjsService.syncWithTheLatestRevisionForce(pageId);
-        return res.apiv3({ });
+        const result = await yjsService.syncWithTheLatestRevisionForce(pageId, editingMarkdownLength);
+        return res.apiv3(result);
       }
       }
       catch (err) {
       catch (err) {
         logger.error(err);
         logger.error(err);

+ 7 - 2
apps/app/src/server/service/yjs/yjs.ts

@@ -32,7 +32,7 @@ type RequestWithUser = IncomingMessage & { user: IUserHasId };
 
 
 export interface IYjsService {
 export interface IYjsService {
   getYDocStatus(pageId: string): Promise<YDocStatus>;
   getYDocStatus(pageId: string): Promise<YDocStatus>;
-  syncWithTheLatestRevisionForce(pageId: string): Promise<void>;
+  syncWithTheLatestRevisionForce(pageId: string, editingMarkdownLength?: number): Promise<{ isYjsDataCorrupted?: boolean } | void>
   getCurrentYdoc(pageId: string): Ydoc | undefined;
   getCurrentYdoc(pageId: string): Ydoc | undefined;
 }
 }
 
 
@@ -180,7 +180,7 @@ class YjsService implements IYjsService {
     return YDocStatus.OUTDATED;
     return YDocStatus.OUTDATED;
   }
   }
 
 
-  public async syncWithTheLatestRevisionForce(pageId: string): Promise<void> {
+  public async syncWithTheLatestRevisionForce(pageId: string, editingMarkdownLength?: number): Promise<{ isYjsDataCorrupted?: boolean } | void> {
     const doc = this.ysocketio.documents.get(pageId);
     const doc = this.ysocketio.documents.get(pageId);
 
 
     if (doc == null) {
     if (doc == null) {
@@ -188,6 +188,11 @@ class YjsService implements IYjsService {
     }
     }
 
 
     syncYDoc(this.mdb, doc, true);
     syncYDoc(this.mdb, doc, true);
+
+    if (editingMarkdownLength != null) {
+      const ytextLength = doc?.getText('codemirror').length;
+      return { isYjsDataCorrupted: editingMarkdownLength !== ytextLength };
+    }
   }
   }
 
 
   public getCurrentYdoc(pageId: string): Ydoc | undefined {
   public getCurrentYdoc(pageId: string): Ydoc | undefined {