Просмотр исходного кода

Merge pull request #8971 from weseek/feat/150544-alerts-when-trying-to-sync-with-latest-revision-when-yjs-data-is-corrupt

feat: Alerts when trying to sync with latest revision when yjs data is corrupt
Shun Miyazawa 1 год назад
Родитель
Сommit
68fb27a141

+ 1 - 0
apps/app/public/static/locales/en_US/translation.json

@@ -853,6 +853,7 @@
   },
   "sync-latest-reevision-body": {
     "confirm": "Delete the draft data being entered into the editor and synchronize the latest text. Are you sure you want to run it?",
+    "alert": "The latest text may not have been synchronized. Please reload and check again.",
     "success-toaster": "Latest text synchronized",
     "error-toaster": "Synchronization of the latest text failed"
   }

+ 1 - 0
apps/app/public/static/locales/fr_FR/translation.json

@@ -844,6 +844,7 @@
   },
   "sync-latest-reevision-body": {
     "confirm": "Delete the draft data being entered into the editor and synchronize the latest text. Are you sure you want to run it?",
+    "alert": "Il se peut que le texte le plus récent n'ait pas été synchronisé. Veuillez recharger et vérifier à nouveau.",
     "success-toaster": "Dernier texte synchronisé",
     "error-toaster": "La synchronisation du dernier texte a échoué"
   }

+ 1 - 0
apps/app/public/static/locales/ja_JP/translation.json

@@ -886,6 +886,7 @@
   },
   "sync-latest-reevision-body": {
     "confirm": "エディターに入力中のドラフトデータを削除して最新の本文を同期します。実行しますか?",
+    "alert": "最新の本文が同期されていない可能性があります。リロードして再度ご確認ください。",
     "success-toaster": "最新の本文を同期しました",
     "error-toaster": "最新の本文の同期に失敗しました"
   }

+ 1 - 0
apps/app/public/static/locales/zh_CN/translation.json

@@ -856,6 +856,7 @@
   },
   "sync-latest-reevision-body": {
     "confirm": "删除输入编辑器的草稿数据,同步最新文本。 您真的想运行它吗?",
+    "alert": "最新文本可能尚未同步。 请重新加载并再次检查。",
     "success-toaster": "同步最新文本",
     "error-toaster": "同步最新文本失败"
   }

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

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

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

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

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

@@ -2,7 +2,7 @@ import type { IPage, IUserHasId } from '@growi/core';
 import { ErrorV3 } from '@growi/core/dist/models';
 import type { Request, RequestHandler } from 'express';
 import type { ValidationChain } from 'express-validator';
-import { param } from 'express-validator';
+import { param, body } from 'express-validator';
 import mongoose from 'mongoose';
 
 import type Crowi from '~/server/crowi';
@@ -21,7 +21,10 @@ type SyncLatestRevisionBodyToYjsDraftHandlerFactory = (crowi: Crowi) => RequestH
 type ReqParams = {
   pageId: string,
 }
-interface Req extends Request<ReqParams, ApiV3Response> {
+type ReqBody = {
+  editingMarkdownLength?: number,
+}
+interface Req extends Request<ReqParams, ApiV3Response, ReqBody> {
   user: IUserHasId,
 }
 export const syncLatestRevisionBodyToYjsDraftHandlerFactory: SyncLatestRevisionBodyToYjsDraftHandlerFactory = (crowi) => {
@@ -32,6 +35,7 @@ export const syncLatestRevisionBodyToYjsDraftHandlerFactory: SyncLatestRevisionB
   // define validators for req.params
   const validator: ValidationChain[] = [
     param('pageId').isMongoId().withMessage('The param "pageId" must be specified'),
+    body('editingMarkdownLength').optional().isInt().withMessage('The body "editingMarkdownLength" must be integer'),
   ];
 
   return [
@@ -39,6 +43,7 @@ export const syncLatestRevisionBodyToYjsDraftHandlerFactory: SyncLatestRevisionB
     validator, apiV3FormValidator,
     async(req: Req, res: ApiV3Response) => {
       const { pageId } = req.params;
+      const { editingMarkdownLength } = req.body;
 
       // check whether accessible
       if (!(await Page.isAccessiblePageByViewer(pageId, req.user))) {
@@ -47,8 +52,8 @@ export const syncLatestRevisionBodyToYjsDraftHandlerFactory: SyncLatestRevisionB
 
       try {
         const yjsService = getYjsService();
-        await yjsService.syncWithTheLatestRevisionForce(pageId);
-        return res.apiv3({ });
+        const result = await yjsService.syncWithTheLatestRevisionForce(pageId, editingMarkdownLength);
+        return res.apiv3(result);
       }
       catch (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 {
   getYDocStatus(pageId: string): Promise<YDocStatus>;
-  syncWithTheLatestRevisionForce(pageId: string): Promise<void>;
+  syncWithTheLatestRevisionForce(pageId: string, editingMarkdownLength?: number): Promise<{ isYjsDataBroken?: boolean } | void>
   getCurrentYdoc(pageId: string): Ydoc | undefined;
 }
 
@@ -181,14 +181,19 @@ class YjsService implements IYjsService {
     return YDocStatus.OUTDATED;
   }
 
-  public async syncWithTheLatestRevisionForce(pageId: string): Promise<void> {
+  public async syncWithTheLatestRevisionForce(pageId: string, editingMarkdownLength?: number): Promise<{ isYjsDataBroken?: boolean } | void> {
     const doc = this.ysocketio.documents.get(pageId);
 
     if (doc == null) {
       return;
     }
 
+    const ytextLength = doc?.getText('codemirror').length;
     syncYDoc(this.mdb, doc, true);
+
+    if (editingMarkdownLength != null) {
+      return { isYjsDataBroken: editingMarkdownLength !== ytextLength };
+    }
   }
 
   public getCurrentYdoc(pageId: string): Ydoc | undefined {