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

Merge branch 'dev/7.0.x' into feat/142109-reflect-update-results-from-view-in-ydoc

Shun Miyazawa 2 лет назад
Родитель
Сommit
b7eddc2f0c

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

@@ -2,17 +2,16 @@ import React from 'react';
 
 
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 
 
-
 import { useHashChangedEffect } from '~/client/services/side-effects/hash-changed';
 import { useHashChangedEffect } from '~/client/services/side-effects/hash-changed';
 import { usePageUpdatedEffect } from '~/client/services/side-effects/page-updated';
 import { usePageUpdatedEffect } from '~/client/services/side-effects/page-updated';
 import { useIsEditable } from '~/stores/context';
 import { useIsEditable } from '~/stores/context';
+import { useIsLatestRevision } from '~/stores/page';
 import { EditorMode, useEditorMode } from '~/stores/ui';
 import { EditorMode, useEditorMode } from '~/stores/ui';
 
 
 import { LazyRenderer } from '../Common/LazyRenderer';
 import { LazyRenderer } from '../Common/LazyRenderer';
 
 
-
 const PageEditor = dynamic(() => import('../PageEditor'), { ssr: false });
 const PageEditor = dynamic(() => import('../PageEditor'), { ssr: false });
-
+const PageEditorReadOnly = dynamic(() => import('../PageEditor/PageEditorReadOnly').then(mod => mod.PageEditorReadOnly), { ssr: false });
 
 
 type Props = {
 type Props = {
   pageView: JSX.Element,
   pageView: JSX.Element,
@@ -23,6 +22,7 @@ export const DisplaySwitcher = (props: Props): JSX.Element => {
 
 
   const { data: editorMode = EditorMode.View } = useEditorMode();
   const { data: editorMode = EditorMode.View } = useEditorMode();
   const { data: isEditable } = useIsEditable();
   const { data: isEditable } = useIsEditable();
+  const { data: isLatestRevision } = useIsLatestRevision();
 
 
   usePageUpdatedEffect();
   usePageUpdatedEffect();
   useHashChangedEffect();
   useHashChangedEffect();
@@ -34,7 +34,10 @@ export const DisplaySwitcher = (props: Props): JSX.Element => {
       { isViewMode && pageView }
       { isViewMode && pageView }
 
 
       <LazyRenderer shouldRender={isEditable === true && editorMode === EditorMode.Editor}>
       <LazyRenderer shouldRender={isEditable === true && editorMode === EditorMode.Editor}>
-        <PageEditor />
+        { isLatestRevision
+          ? <PageEditor />
+          : <PageEditorReadOnly />
+        }
       </LazyRenderer>
       </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 { returnPathForURL } from '@growi/core/dist/utils/path-utils';
-import Link from 'next/link';
+import { useRouter } from 'next/router';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
-import { useSWRxCurrentPage, useIsLatestRevision } from '~/stores/page';
+import { useSWRxCurrentPage, useSWRMUTxCurrentPage, useIsLatestRevision } from '~/stores/page';
 
 
 export const OldRevisionAlert = (): JSX.Element => {
 export const OldRevisionAlert = (): JSX.Element => {
-
+  const router = useRouter();
   const { t } = useTranslation();
   const { t } = useTranslation();
-  const { data: isLatestRevision } = useIsLatestRevision();
+
+  const { data: isOldRevisionPage } = useIsLatestRevision();
   const { data: page } = useSWRxCurrentPage();
   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 <></>;
   }
   }
 
 
   return (
   return (
     <div className="alert alert-warning">
     <div className="alert alert-warning">
       <strong>{t('Warning')}: </strong> {t('page_page.notice.version')}
       <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')}
         <span className="material-symbols-outlined me-1">arrow_circle_right</span>{t('Show latest')}
-      </Link>
+      </button>
     </div>
     </div>
   );
   );
 };
 };

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

@@ -51,7 +51,7 @@ import loggerFactory from '~/utils/logger';
 import { EditorNavbar } from './EditorNavbar';
 import { EditorNavbar } from './EditorNavbar';
 import EditorNavbarBottom from './EditorNavbarBottom';
 import EditorNavbarBottom from './EditorNavbarBottom';
 import Preview from './Preview';
 import Preview from './Preview';
-import { scrollEditor, scrollPreview } from './ScrollSyncHelper';
+import { useScrollSync } from './ScrollSyncHelper';
 import { useConflictResolver, useConflictEffect, type ConflictHandler } from './conflict';
 import { useConflictResolver, useConflictEffect, type ConflictHandler } from './conflict';
 
 
 import '@growi/editor/dist/style.css';
 import '@growi/editor/dist/style.css';
@@ -65,10 +65,6 @@ declare global {
   var globalEmitter: EventEmitter;
   var globalEmitter: EventEmitter;
 }
 }
 
 
-// for scrolling
-let isOriginOfScrollSyncEditor = false;
-let isOriginOfScrollSyncPreview = false;
-
 export type SaveOptions = {
 export type SaveOptions = {
   slackChannels: string,
   slackChannels: string,
   overwriteScopesOfDescendants?: boolean
   overwriteScopesOfDescendants?: boolean
@@ -163,6 +159,11 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
 
 
   const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.MAIN);
   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) => {
   const save: Save = useCallback(async(revisionId, markdown, opts, onConflict) => {
     if (pageId == null || grantData == null) {
     if (pageId == null || grantData == null) {
       logger.error('Some materials to save are invalid', {
       logger.error('Some materials to save are invalid', {
@@ -272,38 +273,6 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
 
 
   }, [codeMirrorEditor, pageId]);
   }, [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
   // initial caret line
   useEffect(() => {
   useEffect(() => {
     codeMirrorEditor?.setCaretLine();
     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;
 let defaultTop = 0;
 const padding = 5;
 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);
   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);
   setDefaultTop(previewRootElement.getBoundingClientRect().y);
 
 
@@ -150,3 +154,41 @@ export const scrollPreview = (editorRootElement: HTMLElement, previewRootElement
   editorRootElement.scrollTop = newScrollTop;
   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 };
+};

+ 3 - 2
apps/app/src/pages/_private-legacy-pages.page.tsx

@@ -6,6 +6,7 @@ import { useTranslation } from 'next-i18next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
 import Head from 'next/head';
+import { useIsomorphicLayoutEffect } from 'usehooks-ts';
 
 
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
@@ -48,8 +49,8 @@ const PrivateLegacyPage: NextPage<Props> = (props: Props) => {
   useCurrentUser(props.currentUser ?? null);
   useCurrentUser(props.currentUser ?? null);
 
 
   // clear the cache for the current page
   // clear the cache for the current page
-  const { mutate } = useSWRxCurrentPage();
-  mutate(undefined, { revalidate: false });
+  //  in order to fix https://redmine.weseek.co.jp/issues/135811
+  useSWRxCurrentPage(null);
   useCurrentPageId(null);
   useCurrentPageId(null);
   useCurrentPathname('/_private-legacy-pages');
   useCurrentPathname('/_private-legacy-pages');
 
 

+ 7 - 4
apps/app/src/pages/_search.page.tsx

@@ -4,7 +4,9 @@ import type { IUser } from '@growi/core';
 import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
 import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
+import dynamic from 'next/dynamic';
 import Head from 'next/head';
 import Head from 'next/head';
+import { useIsomorphicLayoutEffect } from 'usehooks-ts';
 
 
 import SearchResultLayout from '~/components/Layout/SearchResultLayout';
 import SearchResultLayout from '~/components/Layout/SearchResultLayout';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
@@ -16,8 +18,6 @@ import {
 } from '~/stores/context';
 } from '~/stores/context';
 import { useCurrentPageId, useSWRxCurrentPage } from '~/stores/page';
 import { useCurrentPageId, useSWRxCurrentPage } from '~/stores/page';
 
 
-import { SearchPage } from '../components/SearchPage';
-
 import type { NextPageWithLayout } from './_app.page';
 import type { NextPageWithLayout } from './_app.page';
 import type { CommonProps } from './utils/commons';
 import type { CommonProps } from './utils/commons';
 import {
 import {
@@ -25,6 +25,9 @@ import {
 } from './utils/commons';
 } from './utils/commons';
 
 
 
 
+const SearchPage = dynamic(() => import('../components/SearchPage').then(mod => mod.SearchPage), { ssr: false });
+
+
 type Props = CommonProps & {
 type Props = CommonProps & {
   currentUser: IUser,
   currentUser: IUser,
 
 
@@ -52,8 +55,8 @@ const SearchResultPage: NextPageWithLayout<Props> = (props: Props) => {
   useCurrentUser(props.currentUser ?? null);
   useCurrentUser(props.currentUser ?? null);
 
 
   // clear the cache for the current page
   // clear the cache for the current page
-  const { mutate } = useSWRxCurrentPage();
-  mutate(undefined, { revalidate: false });
+  //  in order to fix https://redmine.weseek.co.jp/issues/135811
+  useSWRxCurrentPage(null);
   useCurrentPageId(null);
   useCurrentPageId(null);
   useCurrentPathname('/_search');
   useCurrentPathname('/_search');
 
 

+ 3 - 2
apps/app/src/pages/me/[[...path]].page.tsx

@@ -8,6 +8,7 @@ import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
 import Head from 'next/head';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
+import { useIsomorphicLayoutEffect } from 'usehooks-ts';
 
 
 import { BasicLayout } from '~/components/Layout/BasicLayout';
 import { BasicLayout } from '~/components/Layout/BasicLayout';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
@@ -99,8 +100,8 @@ const MePage: NextPageWithLayout<Props> = (props: Props) => {
   useCurrentUser(props.currentUser ?? null);
   useCurrentUser(props.currentUser ?? null);
 
 
   // clear the cache for the current page
   // clear the cache for the current page
-  const { mutate } = useSWRxCurrentPage();
-  mutate(undefined, { revalidate: false });
+  //  in order to fix https://redmine.weseek.co.jp/issues/135811
+  useSWRxCurrentPage(null);
   useCurrentPageId(null);
   useCurrentPageId(null);
   useCurrentPathname('/me');
   useCurrentPathname('/me');
 
 

+ 3 - 2
apps/app/src/pages/tags.page.tsx

@@ -7,6 +7,7 @@ import { useTranslation } from 'next-i18next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
 import Head from 'next/head';
+import { useIsomorphicLayoutEffect } from 'usehooks-ts';
 
 
 import { LoadingSpinner } from '~/components/LoadingSpinner';
 import { LoadingSpinner } from '~/components/LoadingSpinner';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
@@ -49,8 +50,8 @@ const TagPage: NextPageWithLayout<CommonProps> = (props: Props) => {
   useCurrentUser(props.currentUser ?? null);
   useCurrentUser(props.currentUser ?? null);
 
 
   // clear the cache for the current page
   // clear the cache for the current page
-  const { mutate } = useSWRxCurrentPage();
-  mutate(undefined, { revalidate: false });
+  //  in order to fix https://redmine.weseek.co.jp/issues/135811
+  useSWRxCurrentPage(null);
   useCurrentPageId(null);
   useCurrentPageId(null);
   useCurrentPathname('/tags');
   useCurrentPathname('/tags');
 
 

+ 6 - 11
apps/app/src/pages/trash.page.tsx

@@ -6,6 +6,7 @@ import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
 import Head from 'next/head';
+import { useIsomorphicLayoutEffect } from 'usehooks-ts';
 
 
 import { PagePathNavSticky } from '~/components/Common/PagePathNav';
 import { PagePathNavSticky } from '~/components/Common/PagePathNav';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
@@ -43,8 +44,10 @@ const TrashPage: NextPageWithLayout<CommonProps> = (props: Props) => {
   useCurrentUser(props.currentUser ?? null);
   useCurrentUser(props.currentUser ?? null);
 
 
   // clear the cache for the current page
   // clear the cache for the current page
-  const { mutate } = useSWRxCurrentPage();
-  mutate(undefined, { revalidate: false });
+  //  in order to fix https://redmine.weseek.co.jp/issues/135811
+  useSWRxCurrentPage(null);
+  useCurrentPageId(null);
+  useCurrentPathname('/trash');
 
 
   useGrowiCloudUri(props.growiCloudUri);
   useGrowiCloudUri(props.growiCloudUri);
 
 
@@ -53,8 +56,6 @@ const TrashPage: NextPageWithLayout<CommonProps> = (props: Props) => {
   useIsSearchScopeChildrenAsDefault(props.isSearchScopeChildrenAsDefault);
   useIsSearchScopeChildrenAsDefault(props.isSearchScopeChildrenAsDefault);
 
 
   useIsSearchPage(false);
   useIsSearchPage(false);
-  useCurrentPageId(null);
-  useCurrentPathname('/trash');
 
 
   // init sidebar config with UserUISettings and sidebarConfig
   // init sidebar config with UserUISettings and sidebarConfig
   useInitSidebarConfig(props.sidebarConfig, props.userUISettings);
   useInitSidebarConfig(props.sidebarConfig, props.userUISettings);
@@ -69,16 +70,10 @@ const TrashPage: NextPageWithLayout<CommonProps> = (props: Props) => {
         <title>{title}</title>
         <title>{title}</title>
       </Head>
       </Head>
       <div className="dynamic-layout-root">
       <div className="dynamic-layout-root">
-        <nav className="sticky-top">
-          TODO: implement navigation for /trash
-        </nav>
-
-        <div className="content-main container-lg mb-5 pb-5">
+        <div className="content-main container-lg mt-5 ms-md-5 ms-xl-0">
           <PagePathNavSticky pagePath="/trash" />
           <PagePathNavSticky pagePath="/trash" />
           <TrashPageList />
           <TrashPageList />
         </div>
         </div>
-
-        <div id="grw-fav-sticky-trigger" className="sticky-top"></div>
       </div>
       </div>
     </>
     </>
   );
   );

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

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

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

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