Kaynağa Gözat

WIP: refactor DisplaySwitcher

Yuki Takei 3 yıl önce
ebeveyn
işleme
a675736f18

+ 4 - 104
packages/app/src/components/Page/DisplaySwitcher.tsx

@@ -1,6 +1,6 @@
-import React, { useMemo } from 'react';
+import React from 'react';
 
-import { type IPagePopulatedToShowRevision, pagePathUtils } from '@growi/core';
+import { type IPagePopulatedToShowRevision } from '@growi/core';
 import dynamic from 'next/dynamic';
 
 
@@ -10,122 +10,22 @@ import { usePageUpdatedEffect } from '~/client/services/side-effects/page-update
 import { useIsEditable } from '~/stores/context';
 import { EditorMode, useEditorMode } from '~/stores/ui';
 
-import type { CommentsProps } from '../Comments';
 import { LazyRenderer } from '../Common/LazyRenderer';
-import { MainPane } from '../Layout/MainPane';
-import { PageAlerts } from '../PageAlert/PageAlerts';
-import { PageContentFooter } from '../PageContentFooter';
-import type { PageSideContentsProps } from '../PageSideContents';
-import { UserInfo } from '../User/UserInfo';
-import type { UsersHomePageFooterProps } from '../UsersHomePageFooter';
 
-const { isUsersHomePage } = pagePathUtils;
+import { PageView } from './PageView';
 
 
-const NotCreatablePage = dynamic(() => import('../NotCreatablePage').then(mod => mod.NotCreatablePage), { ssr: false });
-const ForbiddenPage = dynamic(() => import('../ForbiddenPage'), { ssr: false });
-const NotFoundPage = dynamic(() => import('../NotFoundPage'), { ssr: false });
-const PageSideContents = dynamic<PageSideContentsProps>(() => import('../PageSideContents').then(mod => mod.PageSideContents), { ssr: false });
-const Comments = dynamic<CommentsProps>(() => import('../Comments').then(mod => mod.Comments), { ssr: false });
-const UsersHomePageFooter = dynamic<UsersHomePageFooterProps>(() => import('../UsersHomePageFooter')
-  .then(mod => mod.UsersHomePageFooter), { ssr: false });
-
 const PageEditor = dynamic(() => import('../PageEditor'), { ssr: false });
 const PageEditorByHackmd = dynamic(() => import('../PageEditorByHackmd').then(mod => mod.PageEditorByHackmd), { ssr: false });
 const EditorNavbarBottom = dynamic(() => import('../PageEditor/EditorNavbarBottom'), { ssr: false });
 
 
-const IdenticalPathPage = (): JSX.Element => {
-  const IdenticalPathPage = dynamic(() => import('../IdenticalPathPage').then(mod => mod.IdenticalPathPage), { ssr: false });
-  return <IdenticalPathPage />;
-};
-
-
 type Props = {
   pagePath: string,
   page?: IPagePopulatedToShowRevision,
-  isIdenticalPathPage?: boolean,
-  isNotFound?: boolean,
-  isForbidden?: boolean,
-  isNotCreatable?: boolean,
   ssrBody?: JSX.Element,
 }
 
-const View = (props: Props): JSX.Element => {
-  const {
-    pagePath, page,
-    isIdenticalPathPage, isNotFound, isForbidden, isNotCreatable,
-    ssrBody,
-  } = props;
-
-  const pageId = page?._id;
-
-  const specialContents = useMemo(() => {
-    if (isIdenticalPathPage) {
-      return <IdenticalPathPage />;
-    }
-    if (isForbidden) {
-      return <ForbiddenPage />;
-    }
-    if (isNotCreatable) {
-      return <NotCreatablePage />;
-    }
-    if (isNotFound) {
-      return <NotFoundPage />;
-    }
-  }, [isForbidden, isIdenticalPathPage, isNotCreatable, isNotFound]);
-
-  const sideContents = !isNotFound && !isNotCreatable
-    ? (
-      <PageSideContents page={page} />
-    )
-    : <></>;
-
-  const footerContents = !isIdenticalPathPage && !isNotFound && page != null
-    ? (
-      <>
-        { pageId != null && pagePath != null && (
-          <Comments pageId={pageId} pagePath={pagePath} revision={page.revision} />
-        ) }
-        { pagePath != null && isUsersHomePage(pagePath) && (
-          <UsersHomePageFooter creatorId={page.creator._id}/>
-        ) }
-        <PageContentFooter page={page} />
-      </>
-    )
-    : <></>;
-
-  const isUsersHomePagePath = isUsersHomePage(pagePath);
-
-  const contents = specialContents != null
-    ? <></>
-    : (() => {
-      const Page = dynamic(() => import('./Page').then(mod => mod.Page), {
-        ssr: false,
-        loading: () => ssrBody ?? <></>,
-      });
-      return <Page />;
-    })();
-
-  return (
-    <MainPane
-      sideContents={sideContents}
-      footerContents={footerContents}
-    >
-      <PageAlerts />
-
-      { specialContents }
-      { specialContents == null && (
-        <>
-          { isUsersHomePagePath && <UserInfo author={page?.creator} /> }
-          { contents }
-        </>
-      ) }
-
-    </MainPane>
-  );
-};
-
 export const DisplaySwitcher = (props: Props): JSX.Element => {
 
   const { data: editorMode = EditorMode.View } = useEditorMode();
@@ -139,7 +39,7 @@ export const DisplaySwitcher = (props: Props): JSX.Element => {
 
   return (
     <>
-      { isViewMode && <View {...props} /> }
+      { isViewMode && <PageView {...props} /> }
 
       <LazyRenderer shouldRender={isEditable === true && editorMode === EditorMode.Editor}>
         <div data-testid="page-editor" id="page-editor" className="editor-root">

+ 124 - 0
packages/app/src/components/Page/PageContents.tsx

@@ -0,0 +1,124 @@
+import React, {
+  useCallback, useEffect, useRef,
+} from 'react';
+
+import { pagePathUtils } from '@growi/core';
+import { useTranslation } from 'next-i18next';
+import { HtmlElementNode } from 'rehype-toc';
+
+import { useDrawioModalLauncherForView } from '~/client/services/side-effects/drawio-modal-launcher-for-view';
+import { useHandsontableModalLauncherForView } from '~/client/services/side-effects/handsontable-modal-launcher-for-view';
+import { toastSuccess, toastError } from '~/client/util/toastr';
+import {
+  useIsGuestUser, useCurrentPathname,
+} from '~/stores/context';
+import { useEditingMarkdown } from '~/stores/editor';
+import { useSWRxCurrentPage } from '~/stores/page';
+import { useViewOptions } from '~/stores/renderer';
+import {
+  useCurrentPageTocNode,
+  useIsMobile,
+} from '~/stores/ui';
+import { registerGrowiFacade } from '~/utils/growi-facade';
+import loggerFactory from '~/utils/logger';
+
+import RevisionRenderer from './RevisionRenderer';
+
+import styles from './Page.module.scss';
+
+const logger = loggerFactory('growi:Page');
+
+
+export const PageContents = (): JSX.Element => {
+  const { t } = useTranslation();
+
+  // Pass tocRef to generateViewOptions (=> rehypePlugin => customizeTOC) to call mutateCurrentPageTocNode when tocRef.current changes.
+  // The toc node passed by customizeTOC is assigned to tocRef.current.
+  const tocRef = useRef<HtmlElementNode>();
+
+  const storeTocNodeHandler = useCallback((toc: HtmlElementNode) => {
+    tocRef.current = toc;
+  }, []);
+
+  const { data: currentPathname } = useCurrentPathname();
+  const isSharedPage = pagePathUtils.isSharedPage(currentPathname ?? '');
+
+  const { data: currentPage, mutate: mutateCurrentPage } = useSWRxCurrentPage();
+  const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
+  const { data: isGuestUser } = useIsGuestUser();
+  const { data: isMobile } = useIsMobile();
+  const { data: rendererOptions, mutate: mutateRendererOptions } = useViewOptions(storeTocNodeHandler);
+  const { mutate: mutateCurrentPageTocNode } = useCurrentPageTocNode();
+
+
+  // register to facade
+  useEffect(() => {
+    registerGrowiFacade({
+      markdownRenderer: {
+        optionsMutators: {
+          viewOptionsMutator: mutateRendererOptions,
+        },
+      },
+    });
+  }, [mutateRendererOptions]);
+
+  useEffect(() => {
+    mutateCurrentPageTocNode(tocRef.current);
+  // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [mutateCurrentPageTocNode, tocRef.current]); // include tocRef.current to call mutateCurrentPageTocNode when tocRef.current changes
+
+
+  useHandsontableModalLauncherForView({
+    onSaveSuccess: (newMarkdown) => {
+      toastSuccess(t('toaster.save_succeeded'));
+
+      // rerender
+      if (!isSharedPage) {
+        mutateCurrentPage();
+      }
+      mutateEditingMarkdown(newMarkdown);
+    },
+    onSaveError: (error) => {
+      toastError(error);
+    },
+  });
+
+  useDrawioModalLauncherForView({
+    onSaveSuccess: (newMarkdown) => {
+      toastSuccess(t('toaster.save_succeeded'));
+
+      // rerender
+      if (!isSharedPage) {
+        mutateCurrentPage();
+      }
+      mutateEditingMarkdown(newMarkdown);
+    },
+    onSaveError: (error) => {
+      toastError(error);
+    },
+  });
+
+
+  if (currentPage == null || rendererOptions == null) {
+    const entries = Object.entries({
+      currentPage, isGuestUser, rendererOptions,
+    })
+      .map(([key, value]) => [key, value == null ? 'null' : undefined])
+      .filter(([, value]) => value != null);
+
+    logger.warn('Some of materials are missing.', Object.fromEntries(entries));
+
+    return <></>;
+  }
+
+  const { _id: revisionId, body: markdown } = currentPage.revision;
+
+  return (
+    <div className={`mb-5 ${isMobile ? `page-mobile ${styles['page-mobile']}` : ''}`}>
+      { revisionId != null && (
+        <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown} />
+      )}
+    </div>
+  );
+
+};

+ 118 - 0
packages/app/src/components/Page/PageView.tsx

@@ -0,0 +1,118 @@
+import React, { useMemo } from 'react';
+
+import { type IPagePopulatedToShowRevision, pagePathUtils } from '@growi/core';
+import dynamic from 'next/dynamic';
+
+
+import {
+  useIsForbidden, useIsIdenticalPath, useIsNotCreatable, useIsNotFound,
+} from '~/stores/context';
+
+import type { CommentsProps } from '../Comments';
+import { MainPane } from '../Layout/MainPane';
+import { PageAlerts } from '../PageAlert/PageAlerts';
+import { PageContentFooter } from '../PageContentFooter';
+import type { PageSideContentsProps } from '../PageSideContents';
+import { UserInfo } from '../User/UserInfo';
+import type { UsersHomePageFooterProps } from '../UsersHomePageFooter';
+
+const { isUsersHomePage } = pagePathUtils;
+
+
+const NotCreatablePage = dynamic(() => import('../NotCreatablePage').then(mod => mod.NotCreatablePage), { ssr: false });
+const ForbiddenPage = dynamic(() => import('../ForbiddenPage'), { ssr: false });
+const NotFoundPage = dynamic(() => import('../NotFoundPage'), { ssr: false });
+const PageSideContents = dynamic<PageSideContentsProps>(() => import('../PageSideContents').then(mod => mod.PageSideContents), { ssr: false });
+const Comments = dynamic<CommentsProps>(() => import('../Comments').then(mod => mod.Comments), { ssr: false });
+const UsersHomePageFooter = dynamic<UsersHomePageFooterProps>(() => import('../UsersHomePageFooter')
+  .then(mod => mod.UsersHomePageFooter), { ssr: false });
+
+const IdenticalPathPage = (): JSX.Element => {
+  const IdenticalPathPage = dynamic(() => import('../IdenticalPathPage').then(mod => mod.IdenticalPathPage), { ssr: false });
+  return <IdenticalPathPage />;
+};
+
+
+type Props = {
+  pagePath: string,
+  page?: IPagePopulatedToShowRevision,
+  ssrBody?: JSX.Element,
+}
+
+export const PageView = (props: Props): JSX.Element => {
+  const {
+    pagePath, page, ssrBody,
+  } = props;
+
+  const pageId = page?._id;
+
+  const { data: isIdenticalPathPage } = useIsIdenticalPath();
+  const { data: isForbidden } = useIsForbidden();
+  const { data: isNotCreatable } = useIsNotCreatable();
+  const { data: isNotFound } = useIsNotFound();
+
+  const specialContents = useMemo(() => {
+    if (isIdenticalPathPage) {
+      return <IdenticalPathPage />;
+    }
+    if (isForbidden) {
+      return <ForbiddenPage />;
+    }
+    if (isNotCreatable) {
+      return <NotCreatablePage />;
+    }
+    if (isNotFound) {
+      return <NotFoundPage />;
+    }
+  }, [isForbidden, isIdenticalPathPage, isNotCreatable, isNotFound]);
+
+  const sideContents = !isNotFound && !isNotCreatable
+    ? (
+      <PageSideContents page={page} />
+    )
+    : <></>;
+
+  const footerContents = !isIdenticalPathPage && !isNotFound && page != null
+    ? (
+      <>
+        { pageId != null && pagePath != null && (
+          <Comments pageId={pageId} pagePath={pagePath} revision={page.revision} />
+        ) }
+        { pagePath != null && isUsersHomePage(pagePath) && (
+          <UsersHomePageFooter creatorId={page.creator._id}/>
+        ) }
+        <PageContentFooter page={page} />
+      </>
+    )
+    : <></>;
+
+  const isUsersHomePagePath = isUsersHomePage(pagePath);
+
+  const contents = specialContents != null
+    ? <></>
+    : (() => {
+      const PageContents = dynamic(() => import('./PageContents').then(mod => mod.PageContents), {
+        ssr: false,
+        loading: () => ssrBody ?? <></>,
+      });
+      return <PageContents />;
+    })();
+
+  return (
+    <MainPane
+      sideContents={sideContents}
+      footerContents={footerContents}
+    >
+      <PageAlerts />
+
+      { specialContents }
+      { specialContents == null && (
+        <>
+          { isUsersHomePagePath && <UserInfo author={page?.creator} /> }
+          { contents }
+        </>
+      ) }
+
+    </MainPane>
+  );
+};