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

Merge pull request #8582 from weseek/fix/140188-cannot-view-past-revisionbody-in-editor

fix: Cannot view past revisionBody in editor
Yuki Takei 2 лет назад
Родитель
Сommit
1c68821382

+ 7 - 4
apps/app/src/components/Page/DisplaySwitcher.tsx

@@ -2,17 +2,16 @@ import React from 'react';
 
 import dynamic from 'next/dynamic';
 
-
 import { useHashChangedEffect } from '~/client/services/side-effects/hash-changed';
 import { usePageUpdatedEffect } from '~/client/services/side-effects/page-updated';
 import { useIsEditable } from '~/stores/context';
+import { useIsLatestRevision } from '~/stores/page';
 import { EditorMode, useEditorMode } from '~/stores/ui';
 
 import { LazyRenderer } from '../Common/LazyRenderer';
 
-
 const PageEditor = dynamic(() => import('../PageEditor'), { ssr: false });
-
+const PageEditorReadOnly = dynamic(() => import('../PageEditor/PageEditorReadOnly').then(mod => mod.PageEditorReadOnly), { ssr: false });
 
 type Props = {
   pageView: JSX.Element,
@@ -23,6 +22,7 @@ export const DisplaySwitcher = (props: Props): JSX.Element => {
 
   const { data: editorMode = EditorMode.View } = useEditorMode();
   const { data: isEditable } = useIsEditable();
+  const { data: isLatestRevision } = useIsLatestRevision();
 
   usePageUpdatedEffect();
   useHashChangedEffect();
@@ -34,7 +34,10 @@ export const DisplaySwitcher = (props: Props): JSX.Element => {
       { isViewMode && pageView }
 
       <LazyRenderer shouldRender={isEditable === true && editorMode === EditorMode.Editor}>
-        <PageEditor />
+        { isLatestRevision
+          ? <PageEditor />
+          : <PageEditorReadOnly />
+        }
       </LazyRenderer>
     </>
   );

+ 20 - 8
apps/app/src/components/PageAlert/OldRevisionAlert.tsx

@@ -1,27 +1,39 @@
-import React from 'react';
+import React, { useCallback } from 'react';
 
 import { returnPathForURL } from '@growi/core/dist/utils/path-utils';
-import Link from 'next/link';
+import { useRouter } from 'next/router';
 import { useTranslation } from 'react-i18next';
 
-import { useSWRxCurrentPage, useIsLatestRevision } from '~/stores/page';
+import { useSWRxCurrentPage, useSWRMUTxCurrentPage, useIsLatestRevision } from '~/stores/page';
 
 export const OldRevisionAlert = (): JSX.Element => {
-
+  const router = useRouter();
   const { t } = useTranslation();
-  const { data: isLatestRevision } = useIsLatestRevision();
+
+  const { data: isOldRevisionPage } = useIsLatestRevision();
   const { data: page } = useSWRxCurrentPage();
+  const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
+
+  const onClickShowLatestButton = useCallback(async() => {
+    if (page == null) {
+      return;
+    }
+
+    const url = returnPathForURL(page.path, page._id);
+    await router.push(url);
+    mutateCurrentPage();
+  }, [mutateCurrentPage, page, router]);
 
-  if (page == null || isLatestRevision == null || isLatestRevision) {
+  if (page == null || isOldRevisionPage) {
     return <></>;
   }
 
   return (
     <div className="alert alert-warning">
       <strong>{t('Warning')}: </strong> {t('page_page.notice.version')}
-      <Link href={returnPathForURL(page.path, page._id)}>
+      <button type="button" className="btn btn-outline-natural-secondary" onClick={onClickShowLatestButton}>
         <span className="material-symbols-outlined me-1">arrow_circle_right</span>{t('Show latest')}
-      </Link>
+      </button>
     </div>
   );
 };

+ 6 - 37
apps/app/src/components/PageEditor/PageEditor.tsx

@@ -51,7 +51,7 @@ import loggerFactory from '~/utils/logger';
 import { EditorNavbar } from './EditorNavbar';
 import EditorNavbarBottom from './EditorNavbarBottom';
 import Preview from './Preview';
-import { scrollEditor, scrollPreview } from './ScrollSyncHelper';
+import { useScrollSync } from './ScrollSyncHelper';
 import { useConflictResolver, useConflictEffect, type ConflictHandler } from './conflict';
 
 import '@growi/editor/dist/style.css';
@@ -65,10 +65,6 @@ declare global {
   var globalEmitter: EventEmitter;
 }
 
-// for scrolling
-let isOriginOfScrollSyncEditor = false;
-let isOriginOfScrollSyncPreview = false;
-
 export type SaveOptions = {
   slackChannels: string,
   overwriteScopesOfDescendants?: boolean
@@ -163,6 +159,11 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
 
   const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.MAIN);
 
+  const { scrollEditorHandler, scrollPreviewHandler } = useScrollSync(GlobalCodeMirrorEditorKey.MAIN, previewRef);
+
+  const scrollEditorHandlerThrottle = useMemo(() => throttle(25, scrollEditorHandler), [scrollEditorHandler]);
+  const scrollPreviewHandlerThrottle = useMemo(() => throttle(25, scrollPreviewHandler), [scrollPreviewHandler]);
+
   const save: Save = useCallback(async(revisionId, markdown, opts, onConflict) => {
     if (pageId == null || grantData == null) {
       logger.error('Some materials to save are invalid', {
@@ -272,38 +273,6 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
 
   }, [codeMirrorEditor, pageId]);
 
-  const scrollEditorHandler = useCallback(() => {
-    if (codeMirrorEditor?.view?.scrollDOM == null || previewRef.current == null) {
-      return;
-    }
-
-    if (isOriginOfScrollSyncPreview) {
-      isOriginOfScrollSyncPreview = false;
-      return;
-    }
-
-    isOriginOfScrollSyncEditor = true;
-    scrollEditor(codeMirrorEditor.view.scrollDOM, previewRef.current);
-  }, [codeMirrorEditor]);
-
-  const scrollEditorHandlerThrottle = useMemo(() => throttle(25, scrollEditorHandler), [scrollEditorHandler]);
-
-  const scrollPreviewHandler = useCallback(() => {
-    if (codeMirrorEditor?.view?.scrollDOM == null || previewRef.current == null) {
-      return;
-    }
-
-    if (isOriginOfScrollSyncEditor) {
-      isOriginOfScrollSyncEditor = false;
-      return;
-    }
-
-    isOriginOfScrollSyncPreview = true;
-    scrollPreview(codeMirrorEditor.view.scrollDOM, previewRef.current);
-  }, [codeMirrorEditor]);
-
-  const scrollPreviewHandlerThrottle = useMemo(() => throttle(25, scrollPreviewHandler), [scrollPreviewHandler]);
-
   // initial caret line
   useEffect(() => {
     codeMirrorEditor?.setCaretLine();

+ 58 - 0
apps/app/src/components/PageEditor/PageEditorReadOnly.tsx

@@ -0,0 +1,58 @@
+import react, { useMemo, useRef } from 'react';
+
+import { CodeMirrorEditorReadOnly, GlobalCodeMirrorEditorKey } from '@growi/editor';
+import { throttle } from 'throttle-debounce';
+
+import { useShouldExpandContent } from '~/client/services/layout';
+import { useSWRxCurrentPage, useIsLatestRevision } from '~/stores/page';
+import { usePreviewOptions } from '~/stores/renderer';
+
+import { EditorNavbar } from './EditorNavbar';
+import Preview from './Preview';
+import { useScrollSync } from './ScrollSyncHelper';
+
+export const PageEditorReadOnly = react.memo((): JSX.Element => {
+  const previewRef = useRef<HTMLDivElement>(null);
+
+  const { data: currentPage } = useSWRxCurrentPage();
+  const { data: rendererOptions } = usePreviewOptions();
+  const { data: isLatestRevision } = useIsLatestRevision();
+  const shouldExpandContent = useShouldExpandContent(currentPage);
+
+  const { scrollEditorHandler, scrollPreviewHandler } = useScrollSync(GlobalCodeMirrorEditorKey.READONLY, previewRef);
+  const scrollEditorHandlerThrottle = useMemo(() => throttle(25, scrollEditorHandler), [scrollEditorHandler]);
+  const scrollPreviewHandlerThrottle = useMemo(() => throttle(25, scrollPreviewHandler), [scrollPreviewHandler]);
+
+  const revisionBody = currentPage?.revision?.body;
+
+  if (rendererOptions == null || isLatestRevision) {
+    return <></>;
+  }
+
+  return (
+    <div id="page-editor" className="flex-expand-vert">
+      <EditorNavbar />
+
+      <div className="flex-expand-horiz">
+        <div className="page-editor-editor-container flex-expand-vert border-end">
+          <CodeMirrorEditorReadOnly
+            markdown={revisionBody}
+            onScroll={scrollEditorHandlerThrottle}
+          />
+        </div>
+        <div
+          ref={previewRef}
+          onScroll={scrollPreviewHandlerThrottle}
+          className="page-editor-preview-container flex-expand-vert overflow-y-auto d-none d-lg-flex"
+        >
+          <Preview
+            markdown={revisionBody}
+            pagePath={currentPage?.path}
+            rendererOptions={rendererOptions}
+            expandContentWidth={shouldExpandContent}
+          />
+        </div>
+      </div>
+    </div>
+  );
+});

+ 44 - 2
apps/app/src/components/PageEditor/ScrollSyncHelper.ts → apps/app/src/components/PageEditor/ScrollSyncHelper.tsx

@@ -1,3 +1,7 @@
+import { useCallback, type RefObject, useRef } from 'react';
+
+import { useCodeMirrorEditorIsolated, type GlobalCodeMirrorEditorKey } from '@growi/editor';
+
 let defaultTop = 0;
 const padding = 5;
 
@@ -88,7 +92,7 @@ const calcScorllElementByRatio = (sourceElement: SourceElement, targetElement: T
 };
 
 
-export const scrollEditor = (editorRootElement: HTMLElement, previewRootElement: HTMLElement): void => {
+const scrollEditor = (editorRootElement: HTMLElement, previewRootElement: HTMLElement): void => {
 
   setDefaultTop(editorRootElement.getBoundingClientRect().top);
 
@@ -120,7 +124,7 @@ export const scrollEditor = (editorRootElement: HTMLElement, previewRootElement:
 
 };
 
-export const scrollPreview = (editorRootElement: HTMLElement, previewRootElement: HTMLElement): void => {
+const scrollPreview = (editorRootElement: HTMLElement, previewRootElement: HTMLElement): void => {
 
   setDefaultTop(previewRootElement.getBoundingClientRect().y);
 
@@ -150,3 +154,41 @@ export const scrollPreview = (editorRootElement: HTMLElement, previewRootElement
   editorRootElement.scrollTop = newScrollTop;
 
 };
+
+// eslint-disable-next-line max-len
+export const useScrollSync = (codeMirrorKey: GlobalCodeMirrorEditorKey, previewRef: RefObject<HTMLDivElement>): { scrollEditorHandler: () => void; scrollPreviewHandler: () => void } => {
+  const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(codeMirrorKey);
+
+  const isOriginOfScrollSyncEditor = useRef(false);
+  const isOriginOfScrollSyncPreview = useRef(false);
+
+  const scrollEditorHandler = useCallback(() => {
+    if (codeMirrorEditor?.view?.scrollDOM == null || previewRef.current == null) {
+      return;
+    }
+
+    if (isOriginOfScrollSyncPreview.current) {
+      isOriginOfScrollSyncPreview.current = false;
+      return;
+    }
+
+    isOriginOfScrollSyncEditor.current = true;
+    scrollEditor(codeMirrorEditor.view.scrollDOM, previewRef.current);
+  }, [codeMirrorEditor, isOriginOfScrollSyncPreview, previewRef]);
+
+  const scrollPreviewHandler = useCallback(() => {
+    if (codeMirrorEditor?.view?.scrollDOM == null || previewRef.current == null) {
+      return;
+    }
+
+    if (isOriginOfScrollSyncEditor.current) {
+      isOriginOfScrollSyncEditor.current = false;
+      return;
+    }
+
+    isOriginOfScrollSyncPreview.current = true;
+    scrollPreview(codeMirrorEditor.view.scrollDOM, previewRef.current);
+  }, [codeMirrorEditor, isOriginOfScrollSyncEditor, previewRef]);
+
+  return { scrollEditorHandler, scrollPreviewHandler };
+};

+ 9 - 5
packages/editor/src/components/CodeMirrorEditor/CodeMirrorEditor.tsx

@@ -41,11 +41,13 @@ export type CodeMirrorEditorProps = {
 
 type Props = CodeMirrorEditorProps & {
   editorKey: string | GlobalCodeMirrorEditorKey,
+  hideToolbar?: boolean,
 }
 
 export const CodeMirrorEditor = (props: Props): JSX.Element => {
   const {
     editorKey,
+    hideToolbar,
     acceptedUploadFileType = AcceptedUploadFileType.NONE,
     indentSize,
     editorSettings,
@@ -209,11 +211,13 @@ export const CodeMirrorEditor = (props: Props): JSX.Element => {
         <FileDropzoneOverlay isEnabled={isDragActive} />
         <CodeMirrorEditorContainer ref={containerRef} />
       </div>
-      <Toolbar
-        editorKey={editorKey}
-        acceptedUploadFileType={acceptedUploadFileType}
-        onUpload={onUpload}
-      />
+      { !hideToolbar && (
+        <Toolbar
+          editorKey={editorKey}
+          acceptedUploadFileType={acceptedUploadFileType}
+          onUpload={onUpload}
+        />
+      ) }
     </div>
   );
 };

+ 39 - 0
packages/editor/src/components/CodeMirrorEditorReadOnly.tsx

@@ -0,0 +1,39 @@
+import { useEffect } from 'react';
+
+import { type Extension, EditorState } from '@codemirror/state';
+
+import { GlobalCodeMirrorEditorKey } from '../consts';
+import { setDataLine } from '../services/extensions/setDataLine';
+import { useCodeMirrorEditorIsolated } from '../stores';
+
+import { CodeMirrorEditor } from '.';
+
+const additionalExtensions: Extension[] = [
+  [
+    setDataLine,
+    EditorState.readOnly.of(true),
+  ],
+];
+
+type Props = {
+  markdown?: string,
+  onScroll?: () => void,
+}
+
+export const CodeMirrorEditorReadOnly = ({ markdown, onScroll }: Props): JSX.Element => {
+  const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.READONLY);
+
+  codeMirrorEditor?.initDoc(markdown);
+
+  useEffect(() => {
+    return codeMirrorEditor?.appendExtensions?.(additionalExtensions);
+  }, [codeMirrorEditor]);
+
+  return (
+    <CodeMirrorEditor
+      hideToolbar
+      editorKey={GlobalCodeMirrorEditorKey.READONLY}
+      onScroll={onScroll}
+    />
+  );
+};

+ 1 - 0
packages/editor/src/components/index.ts

@@ -1,5 +1,6 @@
 export * from './CodeMirrorEditor';
 export * from './CodeMirrorEditorMain';
+export * from './CodeMirrorEditorReadOnly';
 export * from './CodeMirrorEditorComment';
 export * from './CodeMirrorEditorDiff';
 export * from './MergeViewer';

+ 1 - 0
packages/editor/src/consts/global-code-mirror-editor-key.ts

@@ -2,5 +2,6 @@ export const GlobalCodeMirrorEditorKey = {
   MAIN: 'main',
   COMMENT: 'comment',
   DIFF: 'diff',
+  READONLY: 'readonly',
 } as const;
 export type GlobalCodeMirrorEditorKey = typeof GlobalCodeMirrorEditorKey[keyof typeof GlobalCodeMirrorEditorKey]