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

Merge branch 'master' into fix/login-screen-background

ayaka417 3 лет назад
Родитель
Сommit
bff0faf8a9
56 измененных файлов с 581 добавлено и 708 удалено
  1. 5 2
      .github/ISSUE_TEMPLATE/config.yml
  2. 0 25
      .github/ISSUE_TEMPLATE/user-request.md
  3. 1 1
      packages/app/package.json
  4. 5 11
      packages/app/src/client/services/layout.ts
  5. 0 5
      packages/app/src/client/util/smooth-scroll.ts
  6. 52 23
      packages/app/src/components/Comments.tsx
  7. 2 5
      packages/app/src/components/ContentLinkButtons.tsx
  8. 1 2
      packages/app/src/components/Fab.tsx
  9. 0 14
      packages/app/src/components/Invited.module.scss
  10. 0 5
      packages/app/src/components/Layout/NoLoginLayout.module.scss
  11. 0 84
      packages/app/src/components/Page/PageContents.tsx
  12. 39 0
      packages/app/src/components/Page/PageContentsUtilities.tsx
  13. 77 36
      packages/app/src/components/Page/PageView.tsx
  14. 1 1
      packages/app/src/components/PageAlert/PageAlerts.tsx
  15. 3 2
      packages/app/src/components/PageComment/Comment.module.scss
  16. 7 5
      packages/app/src/components/PageComment/Comment.tsx
  17. 8 0
      packages/app/src/components/PageDeleteModal.tsx
  18. 14 4
      packages/app/src/components/PageEditor.tsx
  19. 1 5
      packages/app/src/components/PageEditor/Preview.tsx
  20. 7 10
      packages/app/src/components/PageSideContents.tsx
  21. 14 1
      packages/app/src/components/ReactMarkdownComponents/Header.tsx
  22. 1 8
      packages/app/src/components/ReactMarkdownComponents/NextLink.tsx
  23. 10 10
      packages/app/src/components/SearchPage/SearchResultContent.tsx
  24. 0 58
      packages/app/src/components/ShareLink/ShareLinkPageContents.tsx
  25. 124 0
      packages/app/src/components/ShareLink/ShareLinkPageView.tsx
  26. 5 5
      packages/app/src/components/Sidebar/PageTree/ItemsTree.tsx
  27. 6 17
      packages/app/src/pages/[[...path]].page.tsx
  28. 3 7
      packages/app/src/pages/_private-legacy-pages.page.tsx
  29. 6 12
      packages/app/src/pages/_search.page.tsx
  30. 3 7
      packages/app/src/pages/me/[[...path]].page.tsx
  31. 31 99
      packages/app/src/pages/share/[[...path]].page.tsx
  32. 3 6
      packages/app/src/pages/tags.page.tsx
  33. 7 17
      packages/app/src/pages/trash.page.tsx
  34. 17 3
      packages/app/src/pages/utils/commons.ts
  35. 15 5
      packages/app/src/server/routes/apiv3/users.js
  36. 4 1
      packages/app/src/services/renderer/rehype-plugins/relative-links.ts
  37. 3 3
      packages/app/src/services/renderer/rehype-plugins/relocate-toc.ts
  38. 13 8
      packages/app/src/services/renderer/renderer.tsx
  39. 27 21
      packages/app/src/stores/renderer.tsx
  40. 9 9
      packages/app/src/stores/ui.tsx
  41. 0 3
      packages/app/src/styles/_attachments.scss
  42. 0 27
      packages/app/src/styles/_comment.scss
  43. 0 20
      packages/app/src/styles/_comment_growi.scss
  44. 10 13
      packages/app/src/styles/_editor.scss
  45. 0 6
      packages/app/src/styles/_me.scss
  46. 0 9
      packages/app/src/styles/_old-ios.scss
  47. 0 9
      packages/app/src/styles/_page-duplicate-modal.scss
  48. 0 20
      packages/app/src/styles/_user.scss
  49. 2 0
      packages/app/src/styles/_variables.scss
  50. 2 0
      packages/app/src/styles/organisms/_wiki.scss
  51. 0 7
      packages/app/src/styles/style-app.scss
  52. 14 15
      packages/app/src/styles/theme/_apply-colors.scss
  53. 1 1
      packages/remark-lsx/package.json
  54. 2 2
      packages/remark-lsx/src/components/Lsx.tsx
  55. 9 7
      packages/remark-lsx/src/stores/lsx.tsx
  56. 17 32
      yarn.lock

+ 5 - 2
.github/ISSUE_TEMPLATE/config.yml

@@ -1,5 +1,8 @@
 blank_issues_enabled: false
 contact_links:
-  - name: Question or Suggestions
+  - name: User request or Suggestions
+    url: https://github.com/weseek/growi/discussions
+    about: If you have feature requests or suggestions, you can create a new discussion and consider it with the community.
+  - name: Questions
     url: https://growi-slackin.weseek.co.jp/
-    about: If you have questions or suggestions, you can join our Slack team and talk about anything, anytime.
+    about: If you have questions, you can join our Slack team and talk about anything, anytime.

+ 0 - 25
.github/ISSUE_TEMPLATE/user-request.md

@@ -1,25 +0,0 @@
----
-name: User request
-about: Suggest an idea for this project
-title: 'Request:'
-labels: user requests
----
-
-
-## Informations
-**Is your feature request related to a problem? Please describe.**
-A clear and concise description of what the problem is. 
-
-e.g. I'm having trouble getting immediate access to information
-
-**Describe the solution you'd like**
-A clear and concise description of what you want to realization.
-
-e.g. It's good if there is a space where everyone can access information in common.
-
-**Describe alternatives you've considered**
-A clear and concise description of any alternative solutions or features you've considered.
-
-- [ ] Custom Page In Sidebar
-  - [ ] Can be edited in `/Sidebar` path
-  - [ ] Can be described with md like a page

+ 1 - 1
packages/app/package.json

@@ -185,7 +185,7 @@
     "string-width": "=4.2.2",
     "superjson": "^1.9.1",
     "swagger-jsdoc": "^6.1.0",
-    "swr": "^2.0.2",
+    "swr": "^2.0.3",
     "throttle-debounce": "^3.0.1",
     "toastr": "^2.1.2",
     "uglifycss": "^0.0.29",

+ 5 - 11
packages/app/src/client/services/layout.ts

@@ -1,3 +1,4 @@
+import type { IPage } from '~/interfaces/page';
 import { useIsContainerFluid } from '~/stores/context';
 import { useSWRxCurrentPage } from '~/stores/page';
 import { useEditorMode } from '~/stores/ui';
@@ -5,25 +6,18 @@ import { useEditorMode } from '~/stores/ui';
 export const useEditorModeClassName = (): string => {
   const { getClassNamesByEditorMode } = useEditorMode();
 
-  // TODO: Enable `editing-sidebar` class somehow
-  // https://redmine.weseek.co.jp/issues/111527
-  // const classNames: string[] = [];
-  // if (currentPage != null) {
-  //   const isSidebar = currentPage.path === '/Sidebar';
-  //   classNames.push(...getClassNamesByEditorMode(/* isSidebar */));
-  // }
-
   return `${getClassNamesByEditorMode().join(' ') ?? ''}`;
 };
 
-export const useCurrentGrowiLayoutFluidClassName = (): string => {
+export const useCurrentGrowiLayoutFluidClassName = (initialPage?: IPage): string => {
   const { data: currentPage } = useSWRxCurrentPage();
 
   const { data: dataIsContainerFluid } = useIsContainerFluid();
 
-  const isContainerFluidEachPage = currentPage == null || !('expandContentWidth' in currentPage)
+  const page = currentPage ?? initialPage;
+  const isContainerFluidEachPage = page == null || !('expandContentWidth' in page)
     ? null
-    : currentPage.expandContentWidth;
+    : page.expandContentWidth;
   const isContainerFluidDefault = dataIsContainerFluid;
   const isContainerFluid = isContainerFluidEachPage ?? isContainerFluidDefault;
 

+ 0 - 5
packages/app/src/client/util/smooth-scroll.ts

@@ -1,5 +0,0 @@
-// option object for react-scroll
-export const DEFAULT_AUTO_SCROLL_OPTS = {
-  smooth: 'easeOutQuint',
-  duration: 1200,
-};

+ 52 - 23
packages/app/src/components/Comments.tsx

@@ -1,9 +1,9 @@
-import React from 'react';
+import React, { useEffect, useRef } from 'react';
 
 import { type IRevisionHasId, pagePathUtils } from '@growi/core';
 import dynamic from 'next/dynamic';
 
-import type { PageCommentProps } from '~/components/PageComment';
+import { ROOT_ELEM_ID as PageCommentRootElemId, type PageCommentProps } from '~/components/PageComment';
 import { useSWRxPageComment } from '~/stores/comment';
 import { useIsTrashPage } from '~/stores/page';
 
@@ -22,16 +22,47 @@ export type CommentsProps = {
   pageId: string,
   pagePath: string,
   revision: IRevisionHasId,
+  onLoaded?: () => void,
 }
 
 export const Comments = (props: CommentsProps): JSX.Element => {
 
-  const { pageId, pagePath, revision } = props;
+  const {
+    pageId, pagePath, revision, onLoaded,
+  } = props;
 
   const { mutate } = useSWRxPageComment(pageId);
   const { data: isDeleted } = useIsTrashPage();
   const { data: currentUser } = useCurrentUser();
 
+  const pageCommentParentRef = useRef<HTMLDivElement>(null);
+
+  useEffect(() => {
+    const parent = pageCommentParentRef.current;
+    if (parent == null) return;
+
+    const observerCallback = (mutationRecords:MutationRecord[]) => {
+      mutationRecords.forEach((record:MutationRecord) => {
+        const target = record.target as HTMLElement;
+
+        for (const child of Array.from(target.children)) {
+          const childId = (child as HTMLElement).id;
+          if (childId === PageCommentRootElemId) {
+            onLoaded?.();
+            break;
+          }
+        }
+
+      });
+    };
+
+    const observer = new MutationObserver(observerCallback);
+    observer.observe(parent, { childList: true });
+    return () => {
+      observer.disconnect();
+    };
+  }, [onLoaded]);
+
   const isTopPagePath = isTopPage(pagePath);
 
   if (pageId == null || isTopPagePath) {
@@ -41,29 +72,27 @@ export const Comments = (props: CommentsProps): JSX.Element => {
   return (
     <div className="page-comments-row mt-5 py-4 d-edit-none d-print-none">
       <div className="container-lg">
-        <div className="page-comments">
-          <div id="page-comments-list" className="page-comments-list">
-            <PageComment
+        <div id="page-comments-list" className="page-comments-list" ref={pageCommentParentRef}>
+          <PageComment
+            pageId={pageId}
+            pagePath={pagePath}
+            revision={revision}
+            currentUser={currentUser}
+            isReadOnly={false}
+            titleAlign="left"
+            hideIfEmpty={false}
+          />
+        </div>
+        { !isDeleted && (
+          <div id="page-comment-write">
+            <CommentEditor
               pageId={pageId}
-              pagePath={pagePath}
-              revision={revision}
-              currentUser={currentUser}
-              isReadOnly={false}
-              titleAlign="left"
-              hideIfEmpty={false}
+              isForNewComment
+              onCommentButtonClicked={mutate}
+              revisionId={revision._id}
             />
           </div>
-          { !isDeleted && (
-            <div id="page-comment-write">
-              <CommentEditor
-                pageId={pageId}
-                isForNewComment
-                onCommentButtonClicked={mutate}
-                revisionId={revision._id}
-              />
-            </div>
-          )}
-        </div>
+        )}
       </div>
     </div>
   );

+ 2 - 5
packages/app/src/components/ContentLinkButtons.tsx

@@ -3,17 +3,14 @@ import React from 'react';
 import { IUserHasId } from '@growi/core';
 import { Link as ScrollLink } from 'react-scroll';
 
-import { DEFAULT_AUTO_SCROLL_OPTS } from '~/client/util/smooth-scroll';
 import { RecentlyCreatedIcon } from '~/components/Icons/RecentlyCreatedIcon';
 
 import styles from './ContentLinkButtons.module.scss';
 
-const OFFSET = -120;
-
 const BookMarkLinkButton = React.memo(() => {
 
   return (
-    <ScrollLink to="bookmarks-list" offset={OFFSET} {...DEFAULT_AUTO_SCROLL_OPTS}>
+    <ScrollLink to="bookmarks-list" offset={-120}>
       <button
         type="button"
         className="btn btn-outline-secondary btn-sm px-2"
@@ -30,7 +27,7 @@ BookMarkLinkButton.displayName = 'BookMarkLinkButton';
 const RecentlyCreatedLinkButton = React.memo(() => {
 
   return (
-    <ScrollLink to="recently-created-list" offset={OFFSET} {...DEFAULT_AUTO_SCROLL_OPTS}>
+    <ScrollLink to="recently-created-list" offset={-120}>
       <button
         type="button"
         className="btn btn-outline-secondary btn-sm px-3"

+ 1 - 2
packages/app/src/components/Fab.tsx

@@ -6,7 +6,6 @@ import { animateScroll } from 'react-scroll';
 import { useRipple } from 'react-use-ripple';
 import StickyEvents from 'sticky-events';
 
-import { DEFAULT_AUTO_SCROLL_OPTS } from '~/client/util/smooth-scroll';
 import { usePageCreateModal } from '~/stores/modal';
 import { useCurrentPagePath } from '~/stores/page';
 import { useIsAbleToChangeEditorMode } from '~/stores/ui';
@@ -96,7 +95,7 @@ export const Fab = (): JSX.Element => {
 
   const ScrollToTopButton = useCallback(() => {
     const clickHandler = () => {
-      animateScroll.scrollToTop(DEFAULT_AUTO_SCROLL_OPTS);
+      animateScroll.scrollToTop({ duration: 200 });
     };
 
     return (

+ 0 - 14
packages/app/src/components/Invited.module.scss

@@ -1,14 +0,0 @@
-.invited,
-.nologin.error {
-  .main .row {
-    @media (min-width: 510px) {
-      .offset-sm-4 {
-        margin-left: calc(50% - 240px);
-      }
-
-      .col-sm-4 {
-        width: 480px;
-      }
-    }
-  }
-}

+ 0 - 5
packages/app/src/components/Layout/NoLoginLayout.module.scss

@@ -14,11 +14,6 @@
     .main {
       width: 100vw;
 
-      > .row {
-        margin-right: 20px;
-        margin-left: 20px;
-      }
-
       .nologin-header {
         display: flex;
         flex-direction: column;

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

@@ -1,84 +0,0 @@
-import React, { useEffect } from 'react';
-
-import { useTranslation } from 'next-i18next';
-
-import { useUpdateStateAfterSave } from '~/client/services/page-operation';
-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 { useSWRxCurrentPage } from '~/stores/page';
-import { useViewOptions } from '~/stores/renderer';
-import { registerGrowiFacade } from '~/utils/growi-facade';
-import loggerFactory from '~/utils/logger';
-
-import RevisionRenderer from './RevisionRenderer';
-
-
-const logger = loggerFactory('growi:Page');
-
-
-export const PageContents = (): JSX.Element => {
-  const { t } = useTranslation();
-
-  const { data: currentPage } = useSWRxCurrentPage();
-  const updateStateAfterSave = useUpdateStateAfterSave(currentPage?._id);
-
-  const { data: rendererOptions, mutate: mutateRendererOptions } = useViewOptions();
-
-  // register to facade
-  useEffect(() => {
-    registerGrowiFacade({
-      markdownRenderer: {
-        optionsMutators: {
-          viewOptionsMutator: mutateRendererOptions,
-        },
-      },
-    });
-  }, [mutateRendererOptions]);
-
-  useHandsontableModalLauncherForView({
-    onSaveSuccess: () => {
-      toastSuccess(t('toaster.save_succeeded'));
-
-      updateStateAfterSave?.();
-    },
-    onSaveError: (error) => {
-      toastError(error);
-    },
-  });
-
-  useDrawioModalLauncherForView({
-    onSaveSuccess: () => {
-      toastSuccess(t('toaster.save_succeeded'));
-
-      updateStateAfterSave?.();
-    },
-    onSaveError: (error) => {
-      toastError(error);
-    },
-  });
-
-
-  if (currentPage == null || rendererOptions == null) {
-    const entries = Object.entries({
-      currentPage, 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 (
-    <>
-      { revisionId != null && (
-        <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown} />
-      )}
-    </>
-  );
-
-};

+ 39 - 0
packages/app/src/components/Page/PageContentsUtilities.tsx

@@ -0,0 +1,39 @@
+import { useTranslation } from 'next-i18next';
+
+import { useUpdateStateAfterSave } from '~/client/services/page-operation';
+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 { useCurrentPageId } from '~/stores/context';
+
+
+export const PageContentsUtilities = (): null => {
+  const { t } = useTranslation();
+
+  const { data: pageId } = useCurrentPageId();
+  const updateStateAfterSave = useUpdateStateAfterSave(pageId);
+
+  useHandsontableModalLauncherForView({
+    onSaveSuccess: () => {
+      toastSuccess(t('toaster.save_succeeded'));
+
+      updateStateAfterSave?.();
+    },
+    onSaveError: (error) => {
+      toastError(error);
+    },
+  });
+
+  useDrawioModalLauncherForView({
+    onSaveSuccess: () => {
+      toastSuccess(t('toaster.save_succeeded'));
+
+      updateStateAfterSave?.();
+    },
+    onSaveError: (error) => {
+      toastError(error);
+    },
+  });
+
+  return null;
+};

+ 77 - 36
packages/app/src/components/Page/PageView.tsx

@@ -1,13 +1,21 @@
-import React, { useMemo } from 'react';
+import React, {
+  useEffect, useMemo, useRef, useState,
+} from 'react';
+
 
 import { type IPagePopulatedToShowRevision, pagePathUtils } from '@growi/core';
 import dynamic from 'next/dynamic';
 
-
+import type { RendererConfig } from '~/interfaces/services/renderer';
+import { generateSSRViewOptions } from '~/services/renderer/renderer';
 import {
   useIsForbidden, useIsIdenticalPath, useIsNotCreatable, useIsNotFound,
 } from '~/stores/context';
+import { useSWRxCurrentPage } from '~/stores/page';
+import { useViewOptions } from '~/stores/renderer';
 import { useIsMobile } from '~/stores/ui';
+import { registerGrowiFacade } from '~/utils/growi-facade';
+
 
 import type { CommentsProps } from '../Comments';
 import { MainPane } from '../Layout/MainPane';
@@ -17,7 +25,7 @@ import type { PageSideContentsProps } from '../PageSideContents';
 import { UserInfo } from '../User/UserInfo';
 import type { UsersHomePageFooterProps } from '../UsersHomePageFooter';
 
-import { PageContents } from './PageContents';
+import RevisionRenderer from './RevisionRenderer';
 
 import styles from './PageView.module.scss';
 
@@ -29,35 +37,69 @@ const NotCreatablePage = dynamic(() => import('../NotCreatablePage').then(mod =>
 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 PageContentsUtilities = dynamic(() => import('./PageContentsUtilities').then(mod => mod.PageContentsUtilities), { 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 />;
-};
+const IdenticalPathPage = dynamic(() => import('../IdenticalPathPage').then(mod => mod.IdenticalPathPage), { ssr: false });
 
 
 type Props = {
   pagePath: string,
-  page?: IPagePopulatedToShowRevision,
-  ssrBody?: JSX.Element,
+  rendererConfig: RendererConfig,
+  initialPage?: IPagePopulatedToShowRevision,
 }
 
 export const PageView = (props: Props): JSX.Element => {
+
+  const commentsContainerRef = useRef<HTMLDivElement>(null);
+
+  const [isCommentsLoaded, setCommentsLoaded] = useState(false);
+
   const {
-    pagePath, page, ssrBody,
+    pagePath, initialPage, rendererConfig,
   } = props;
 
-  const pageId = page?._id;
-
   const { data: isIdenticalPathPage } = useIsIdenticalPath();
   const { data: isForbidden } = useIsForbidden();
   const { data: isNotCreatable } = useIsNotCreatable();
-  const { data: isNotFound } = useIsNotFound();
+  const { data: isNotFoundMeta } = useIsNotFound();
   const { data: isMobile } = useIsMobile();
 
+  const { data: pageBySWR } = useSWRxCurrentPage();
+  const { data: viewOptions, mutate: mutateRendererOptions } = useViewOptions();
+
+  const page = pageBySWR ?? initialPage;
+  const isNotFound = isNotFoundMeta || page == null;
+  const isUsersHomePagePath = isUsersHomePage(pagePath);
+
+  // register to facade
+  useEffect(() => {
+    registerGrowiFacade({
+      markdownRenderer: {
+        optionsMutators: {
+          viewOptionsMutator: mutateRendererOptions,
+        },
+      },
+    });
+  }, [mutateRendererOptions]);
+
+  // ***************************  Auto Scroll  ***************************
+  useEffect(() => {
+    // do nothing if hash is empty
+    const { hash } = window.location;
+    if (hash.length === 0) {
+      return;
+    }
+
+    const targetId = hash.slice(1);
+
+    const target = document.getElementById(targetId);
+    target?.scrollIntoView();
+
+  }, [isCommentsLoaded]);
+  // *******************************  end  *******************************
+
   const specialContents = useMemo(() => {
     if (isIdenticalPathPage) {
       return <IdenticalPathPage />;
@@ -68,10 +110,7 @@ export const PageView = (props: Props): JSX.Element => {
     if (isNotCreatable) {
       return <NotCreatablePage />;
     }
-    if (isNotFound) {
-      return <NotFoundPage path={pagePath} />;
-    }
-  }, [isForbidden, isIdenticalPathPage, isNotCreatable, isNotFound, pagePath]);
+  }, [isForbidden, isIdenticalPathPage, isNotCreatable]);
 
   const sideContents = !isNotFound && !isNotCreatable
     ? (
@@ -79,13 +118,13 @@ export const PageView = (props: Props): JSX.Element => {
     )
     : null;
 
-  const footerContents = !isIdenticalPathPage && !isNotFound && page != null
+  const footerContents = !isIdenticalPathPage && !isNotFound
     ? (
       <>
-        { pageId != null && pagePath != null && (
-          <Comments pageId={pageId} pagePath={pagePath} revision={page.revision} />
-        ) }
-        { pagePath != null && isUsersHomePage(pagePath) && (
+        <div id="comments-container" ref={commentsContainerRef}>
+          <Comments pageId={page._id} pagePath={pagePath} revision={page.revision} onLoaded={() => setCommentsLoaded(true)} />
+        </div>
+        { isUsersHomePagePath && (
           <UsersHomePageFooter creatorId={page.creator._id}/>
         ) }
         <PageContentFooter page={page} />
@@ -93,19 +132,21 @@ export const PageView = (props: Props): JSX.Element => {
     )
     : null;
 
-  const isUsersHomePagePath = isUsersHomePage(pagePath);
+  const Contents = () => {
+    if (isNotFound) {
+      return <NotFoundPage path={pagePath} />;
+    }
 
-  const contents = specialContents != null
-    ? <></>
-    // TODO: show SSR body
-    // : (() => {
-    //   const PageContents = dynamic(() => import('./PageContents').then(mod => mod.PageContents), {
-    //     ssr: false,
-    //     // loading: () => ssrBody ?? <></>,
-    //   });
-    //   return <PageContents />;
-    // })();
-    : <PageContents />;
+    const rendererOptions = viewOptions ?? generateSSRViewOptions(rendererConfig, pagePath);
+    const markdown = page.revision.body;
+
+    return (
+      <>
+        <PageContentsUtilities />
+        <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown} />
+      </>
+    );
+  };
 
   return (
     <MainPane
@@ -119,7 +160,7 @@ export const PageView = (props: Props): JSX.Element => {
         <>
           { isUsersHomePagePath && <UserInfo author={page?.creator} /> }
           <div className={`mb-5 ${isMobile ? `page-mobile ${styles['page-mobile']}` : ''}`}>
-            { contents }
+            <Contents />
           </div>
         </>
       ) }

+ 1 - 1
packages/app/src/components/PageAlert/PageAlerts.tsx

@@ -4,12 +4,12 @@ import dynamic from 'next/dynamic';
 
 import { useIsNotFound } from '~/stores/context';
 
-import { FixPageGrantAlert } from './FixPageGrantAlert';
 import { OldRevisionAlert } from './OldRevisionAlert';
 import { PageGrantAlert } from './PageGrantAlert';
 import { PageRedirectedAlert } from './PageRedirectedAlert';
 import { PageStaleAlert } from './PageStaleAlert';
 
+const FixPageGrantAlert = dynamic(() => import('./FixPageGrantAlert').then(mod => mod.FixPageGrantAlert), { ssr: false });
 // dynamic import because TrashPageAlert uses localStorageMiddleware
 const TrashPageAlert = dynamic(() => import('./TrashPageAlert').then(mod => mod.TrashPageAlert), { ssr: false });
 

+ 3 - 2
packages/app/src/components/PageComment/Comment.module.scss

@@ -1,3 +1,4 @@
+@use '../../styles/variables' as var;
 @use '../../styles/bootstrap/init' as bs;
 @use './_comment-inheritance';
 
@@ -10,10 +11,10 @@
 
   .page-comment {
     position: relative;
-    padding-top: 70px;
-    margin-top: -70px;
     pointer-events: none;
 
+    scroll-margin-top: var.$grw-scroll-margin-top-in-view;
+
     // user name
     .page-comment-creator {
       margin-top: -0.5em;

+ 7 - 5
packages/app/src/components/PageComment/Comment.tsx

@@ -80,17 +80,19 @@ export const Comment = (props: CommentProps): JSX.Element => {
     let className = 'page-comment flex-column';
 
     // TODO: fix so that `comment.createdAt` to be type Date https://redmine.weseek.co.jp/issues/113876
-    let commentCreatedAt = comment.createdAt;
-    if (typeof commentCreatedAt === 'string') {
-      commentCreatedAt = parseISO(commentCreatedAt);
-    }
+    const commentCreatedAtFixed = typeof comment.createdAt === 'string'
+      ? parseISO(comment.createdAt)
+      : comment.createdAt;
+    const revisionCreatedAtFixed = typeof revisionCreatedAt === 'string'
+      ? parseISO(revisionCreatedAt)
+      : revisionCreatedAt;
 
     // Conditional for called from SearchResultContext
     if (revisionId != null && revisionCreatedAt != null) {
       if (comment.revision === revisionId) {
         className += ' page-comment-current';
       }
-      else if (commentCreatedAt.getTime() > revisionCreatedAt.getTime()) {
+      else if (commentCreatedAtFixed.getTime() > revisionCreatedAtFixed.getTime()) {
         className += ' page-comment-newer';
       }
       else {

+ 8 - 0
packages/app/src/components/PageDeleteModal.tsx

@@ -83,6 +83,14 @@ const PageDeleteModal: FC = () => {
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
   const [errs, setErrs] = useState<Error[] | null>(null);
 
+  // initialize when opening modal
+  useEffect(() => {
+    if (isOpened) {
+      setIsDeleteRecursively(true);
+      setIsDeleteCompletely(forceDeleteCompletelyMode);
+    }
+  }, [forceDeleteCompletelyMode, isOpened]);
+
   useEffect(() => {
     setIsDeleteCompletely(forceDeleteCompletelyMode);
   }, [forceDeleteCompletelyMode]);

+ 14 - 4
packages/app/src/components/PageEditor.tsx

@@ -16,7 +16,7 @@ import { throttle, debounce } from 'throttle-debounce';
 
 import { useUpdateStateAfterSave, useSaveOrUpdate } from '~/client/services/page-operation';
 import { apiGet, apiPostForm } from '~/client/util/apiv1-client';
-import { toastError, toastSuccess } from '~/client/util/toastr';
+import { toastError, toastSuccess, toastWarning } from '~/client/util/toastr';
 import { IEditorMethods } from '~/interfaces/editor-methods';
 import { OptionsToSave } from '~/interfaces/page-operation';
 import { SocketEventName } from '~/interfaces/websocket';
@@ -218,6 +218,7 @@ const PageEditor = React.memo((): JSX.Element => {
       logger.error('failed to save', error);
       toastError(error);
       if (error.code === 'conflict') {
+        toastWarning('(TBD) resolve conflict');
         // pageContainer.setState({
         //   remoteRevisionId: error.data.revisionId,
         //   remoteRevisionBody: error.data.revisionBody,
@@ -255,11 +256,20 @@ const PageEditor = React.memo((): JSX.Element => {
     }
 
     const page = await save();
-    if (page != null) {
+    if (page == null) {
+      return;
+    }
+
+    if (isNotFound) {
+      await router.push(`/${page._id}#edit`);
+    }
+    else {
       updateStateAfterSave?.();
-      toastSuccess(t('toaster.save_succeeded'));
     }
-  }, [editorMode, save, t, updateStateAfterSave]);
+    toastSuccess(t('toaster.save_succeeded'));
+    mutateEditorMode(EditorMode.Editor);
+
+  }, [editorMode, isNotFound, mutateEditorMode, router, save, t, updateStateAfterSave]);
 
 
   /**

+ 1 - 5
packages/app/src/components/PageEditor/Preview.tsx

@@ -3,7 +3,6 @@ import React, {
 } from 'react';
 
 import { RendererOptions } from '~/services/renderer/renderer';
-import { useEditorSettings } from '~/stores/editor';
 
 import RevisionRenderer from '../Page/RevisionRenderer';
 
@@ -22,12 +21,9 @@ const Preview = React.forwardRef((props: Props, ref: RefObject<HTMLDivElement>):
     markdown, pagePath,
   } = props;
 
-  const { data: editorSettings } = useEditorSettings();
-
-
   return (
     <div
-      className="page-editor-preview-body"
+      className={`page-editor-preview-body ${pagePath === '/Sidebar' ? 'preview-sidebar' : ''}`}
       ref={ref}
       onScroll={(event: SyntheticEvent<HTMLDivElement>) => {
         if (props.onScroll != null) {

+ 7 - 10
packages/app/src/components/PageSideContents.tsx

@@ -4,8 +4,6 @@ import { IPageHasId, pagePathUtils } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { Link } from 'react-scroll';
 
-import { DEFAULT_AUTO_SCROLL_OPTS } from '~/client/util/smooth-scroll';
-import { useCurrentPathname } from '~/stores/context';
 import { useDescendantsPageListModal } from '~/stores/modal';
 
 import CountBadge from './Common/CountBadge';
@@ -20,27 +18,26 @@ const { isTopPage, isUsersHomePage } = pagePathUtils;
 
 
 export type PageSideContentsProps = {
-  page?: IPageHasId,
+  page: IPageHasId,
   isSharedUser?: boolean,
 }
 
 export const PageSideContents = (props: PageSideContentsProps): JSX.Element => {
   const { t } = useTranslation();
 
-  const { data: currentPathname } = useCurrentPathname();
   const { open: openDescendantPageListModal } = useDescendantsPageListModal();
 
   const { page, isSharedUser } = props;
 
-  const pagePath = page?.path ?? currentPathname;
-  const isTopPagePath = isTopPage(pagePath ?? '');
-  const isUsersHomePagePath = isUsersHomePage(pagePath ?? '');
+  const pagePath = page.path;
+  const isTopPagePath = isTopPage(pagePath);
+  const isUsersHomePagePath = isUsersHomePage(pagePath);
 
   return (
     <>
       {/* Page list */}
       <div className={`grw-page-accessories-control ${styles['grw-page-accessories-control']}`}>
-        { pagePath != null && !isSharedUser && (
+        { !isSharedUser && (
           <button
             type="button"
             className="btn btn-block btn-outline-secondary grw-btn-page-accessories rounded-pill d-flex justify-content-between align-items-center"
@@ -57,9 +54,9 @@ export const PageSideContents = (props: PageSideContentsProps): JSX.Element => {
       </div>
 
       {/* Comments */}
-      { page != null && !isTopPagePath && (
+      { !isTopPagePath && (
         <div className={`mt-2 grw-page-accessories-control ${styles['grw-page-accessories-control']}`}>
-          <Link to={'page-comments'} offset={-100} {...DEFAULT_AUTO_SCROLL_OPTS}>
+          <Link to={'page-comments'} offset={-120}>
             <button
               type="button"
               className="btn btn-block btn-outline-secondary grw-btn-page-accessories rounded-pill d-flex justify-content-between align-items-center"

+ 14 - 1
packages/app/src/components/ReactMarkdownComponents/Header.tsx

@@ -85,7 +85,7 @@ export const Header = (props: HeaderProps): JSX.Element => {
     activateByHash(window.location.href);
   }, [activateByHash]);
 
-  // update isActive when hash is changed
+  // update isActive when hash is changed by next router
   useEffect(() => {
     router.events.on('hashChangeComplete', activateByHash);
 
@@ -94,6 +94,19 @@ export const Header = (props: HeaderProps): JSX.Element => {
     };
   }, [activateByHash, router.events]);
 
+  // update isActive when hash is changed
+  useEffect(() => {
+    const activeByHashWrapper = (e: HashChangeEvent) => {
+      activateByHash(e.newURL);
+    };
+
+    window.addEventListener('hashchange', activeByHashWrapper);
+
+    return () => {
+      window.removeEventListener('hashchange', activeByHashWrapper);
+    };
+  }, [activateByHash, router.events]);
+
   const showEditButton = !isGuestUser && !isSharedUser && shareLinkId == null;
 
   return (

+ 1 - 8
packages/app/src/components/ReactMarkdownComponents/NextLink.tsx

@@ -1,7 +1,5 @@
 import Link, { LinkProps } from 'next/link';
-import { Link as ScrollLink } from 'react-scroll';
 
-import { DEFAULT_AUTO_SCROLL_OPTS } from '~/client/util/smooth-scroll';
 import { useSiteUrl } from '~/stores/context';
 import loggerFactory from '~/utils/logger';
 
@@ -42,13 +40,8 @@ export const NextLink = ({
 
   // when href is an anchor link
   if (isAnchorLink(href)) {
-    const to = href.slice(1);
     return (
-      <Link href={href} scroll={false}>
-        <ScrollLink href={href} to={to} className={className} offset={-100} {...DEFAULT_AUTO_SCROLL_OPTS}>
-          {children}
-        </ScrollLink>
-      </Link>
+      <a href={href} className={className}>{children}</a>
     );
   }
 

+ 10 - 10
packages/app/src/components/SearchPage/SearchResultContent.tsx

@@ -11,9 +11,9 @@ import { DropdownItem } from 'reactstrap';
 
 import { exportAsMarkdown, updateContentWidth } from '~/client/services/page-operation';
 import { toastSuccess } from '~/client/util/apiNotification';
-import { IPageToDeleteWithMeta, IPageToRenameWithMeta } from '~/interfaces/page';
-import { IPageWithSearchMeta } from '~/interfaces/search';
-import { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
+import type { IPageToDeleteWithMeta, IPageToRenameWithMeta } from '~/interfaces/page';
+import type { IPageWithSearchMeta } from '~/interfaces/search';
+import type { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
 import { useCurrentUser, useIsContainerFluid } from '~/stores/context';
 import {
   usePageDuplicateModal, usePageRenameModal, usePageDeleteModal,
@@ -22,12 +22,12 @@ import { mutatePageList, mutatePageTree } from '~/stores/page-listing';
 import { useSearchResultOptions } from '~/stores/renderer';
 import { mutateSearching } from '~/stores/search';
 
-import { AdditionalMenuItemsRendererProps, ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
-import { GrowiSubNavigationProps } from '../Navbar/GrowiSubNavigation';
-import { SubNavButtonsProps } from '../Navbar/SubNavButtons';
-import { ROOT_ELEM_ID as RevisionLoaderRoomElemId, RevisionLoaderProps } from '../Page/RevisionLoader';
-import { ROOT_ELEM_ID as PageCommentRootElemId, PageCommentProps } from '../PageComment';
-import { PageContentFooterProps } from '../PageContentFooter';
+import type { AdditionalMenuItemsRendererProps, ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
+import type { GrowiSubNavigationProps } from '../Navbar/GrowiSubNavigation';
+import type { SubNavButtonsProps } from '../Navbar/SubNavButtons';
+import { ROOT_ELEM_ID as RevisionLoaderRoomElemId, type RevisionLoaderProps } from '../Page/RevisionLoader';
+import { ROOT_ELEM_ID as PageCommentRootElemId, type PageCommentProps } from '../PageComment';
+import type { PageContentFooterProps } from '../PageContentFooter';
 
 import styles from './SearchResultContent.module.scss';
 
@@ -96,7 +96,7 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
     const scrollElement = scrollElementRef.current;
     if (scrollElement == null) return;
 
-    const observerCallback = (mutationRecords:MutationRecord[], thisObs: MutationObserver) => {
+    const observerCallback = (mutationRecords:MutationRecord[]) => {
       mutationRecords.forEach((record:MutationRecord) => {
         const target = record.target as HTMLElement;
 

+ 0 - 58
packages/app/src/components/ShareLink/ShareLinkPageContents.tsx

@@ -1,58 +0,0 @@
-import React, { useEffect } from 'react';
-
-import type { IPagePopulatedToShowRevision } from '@growi/core';
-
-import { useViewOptions } from '~/stores/renderer';
-import { registerGrowiFacade } from '~/utils/growi-facade';
-import loggerFactory from '~/utils/logger';
-
-import RevisionRenderer from '../Page/RevisionRenderer';
-
-
-const logger = loggerFactory('growi:Page');
-
-
-export type ShareLinkPageContentsProps = {
-  page?: IPagePopulatedToShowRevision,
-}
-
-export const ShareLinkPageContents = (props: ShareLinkPageContentsProps): JSX.Element => {
-  const { page } = props;
-
-  const { data: rendererOptions, mutate: mutateRendererOptions } = useViewOptions();
-
-  // register to facade
-  useEffect(() => {
-    registerGrowiFacade({
-      markdownRenderer: {
-        optionsMutators: {
-          viewOptionsMutator: mutateRendererOptions,
-        },
-      },
-    });
-  }, [mutateRendererOptions]);
-
-
-  if (page == null || rendererOptions == null) {
-    const entries = Object.entries({
-      page, 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 } = page.revision;
-
-  return (
-    <>
-      { revisionId != null && (
-        <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown} />
-      )}
-    </>
-  );
-
-};

+ 124 - 0
packages/app/src/components/ShareLink/ShareLinkPageView.tsx

@@ -0,0 +1,124 @@
+import React, { useEffect, useMemo } from 'react';
+
+import type { IPagePopulatedToShowRevision } from '@growi/core';
+import dynamic from 'next/dynamic';
+
+import type { RendererConfig } from '~/interfaces/services/renderer';
+import type { IShareLinkHasId } from '~/interfaces/share-link';
+import { generateSSRViewOptions } from '~/services/renderer/renderer';
+import { useIsNotFound } from '~/stores/context';
+import { useViewOptions } from '~/stores/renderer';
+import { registerGrowiFacade } from '~/utils/growi-facade';
+import loggerFactory from '~/utils/logger';
+
+import { MainPane } from '../Layout/MainPane';
+import RevisionRenderer from '../Page/RevisionRenderer';
+import ShareLinkAlert from '../Page/ShareLinkAlert';
+import type { PageSideContentsProps } from '../PageSideContents';
+
+
+const logger = loggerFactory('growi:Page');
+
+
+const PageSideContents = dynamic<PageSideContentsProps>(() => import('../PageSideContents').then(mod => mod.PageSideContents), { ssr: false });
+const ForbiddenPage = dynamic(() => import('../ForbiddenPage'), { ssr: false });
+
+
+type Props = {
+  pagePath: string,
+  rendererConfig: RendererConfig,
+  page?: IPagePopulatedToShowRevision,
+  shareLink?: IShareLinkHasId,
+  isExpired: boolean,
+  disableLinkSharing: boolean,
+}
+
+export const ShareLinkPageView = (props: Props): JSX.Element => {
+  const {
+    pagePath, rendererConfig,
+    page, shareLink,
+    isExpired, disableLinkSharing,
+  } = props;
+
+  const { data: isNotFoundMeta } = useIsNotFound();
+
+  const { data: viewOptions, mutate: mutateRendererOptions } = useViewOptions();
+
+  // register to facade
+  useEffect(() => {
+    registerGrowiFacade({
+      markdownRenderer: {
+        optionsMutators: {
+          viewOptionsMutator: mutateRendererOptions,
+        },
+      },
+    });
+  }, [mutateRendererOptions]);
+
+  const isNotFound = isNotFoundMeta || page == null || shareLink == null;
+
+  const specialContents = useMemo(() => {
+    if (disableLinkSharing) {
+      return <ForbiddenPage isLinkSharingDisabled={props.disableLinkSharing} />;
+    }
+  }, [disableLinkSharing, props.disableLinkSharing]);
+
+  const sideContents = !isNotFound
+    ? (
+      <PageSideContents page={page} />
+    )
+    : null;
+
+
+  const Contents = () => {
+    if (isNotFound) {
+      return <></>;
+    }
+
+    if (isExpired) {
+      return (
+        <>
+          <h2 className="text-muted mt-4">
+            <i className="icon-ban" aria-hidden="true" />
+            <span> Page is expired</span>
+          </h2>
+        </>
+      );
+    }
+
+    const rendererOptions = viewOptions ?? generateSSRViewOptions(rendererConfig, pagePath);
+    const markdown = page.revision.body;
+
+    return (
+      <>
+        <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown} />
+      </>
+    );
+  };
+
+  return (
+    <MainPane
+      sideContents={sideContents}
+    >
+      { specialContents }
+      { specialContents == null && (
+        <>
+          { isNotFound && (
+            <h2 className="text-muted mt-4">
+              <i className="icon-ban" aria-hidden="true" />
+              <span> Page is not found</span>
+            </h2>
+          ) }
+          { !isNotFound && (
+            <>
+              <ShareLinkAlert expiredAt={shareLink.expiredAt} createdAt={shareLink.createdAt} />
+              <div className="mb-5">
+                <Contents />
+              </div>
+            </>
+          ) }
+        </>
+      ) }
+    </MainPane>
+  );
+};

+ 5 - 5
packages/app/src/components/Sidebar/PageTree/ItemsTree.tsx

@@ -2,6 +2,8 @@ import React, {
   useEffect, useRef, useState, useMemo, useCallback,
 } from 'react';
 
+import path from 'path';
+
 import { Nullable } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useRouter } from 'next/router';
@@ -175,13 +177,11 @@ const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
         return;
       }
 
-      const path = pathOrPathsToDelete;
-
       if (isCompletely) {
-        toastSuccess(t('deleted_pages_completely', { path }));
+        toastSuccess(t('deleted_pages_completely', { path: pathOrPathsToDelete }));
       }
       else {
-        toastSuccess(t('deleted_pages', { path }));
+        toastSuccess(t('deleted_pages', { path: pathOrPathsToDelete }));
       }
 
       mutatePageTree();
@@ -191,7 +191,7 @@ const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
 
       if (currentPagePath === pathOrPathsToDelete) {
         mutateCurrentPage();
-        router.push(`/trash${pathOrPathsToDelete}`);
+        router.push(isCompletely ? path.dirname(pathOrPathsToDelete) : `/trash${pathOrPathsToDelete}`);
       }
     };
 

+ 6 - 17
packages/app/src/pages/[[...path]].page.tsx

@@ -21,7 +21,6 @@ import superjson from 'superjson';
 
 import { useCurrentGrowiLayoutFluidClassName, useEditorModeClassName } from '~/client/services/layout';
 import { PageView } from '~/components/Page/PageView';
-import RevisionRenderer from '~/components/Page/RevisionRenderer';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { EditorConfig } from '~/interfaces/editor-settings';
@@ -32,7 +31,6 @@ import type { IUserUISettings } from '~/interfaces/user-ui-settings';
 import type { PageModel, PageDocument } from '~/server/models/page';
 import type { PageRedirectModel } from '~/server/models/page-redirect';
 import type { UserUISettingsModel } from '~/server/models/user-ui-settings';
-import { generateSSRViewOptions } from '~/services/renderer/renderer';
 import { useEditingMarkdown } from '~/stores/editor';
 import { useHasDraftOnHackmd, usePageIdOnHackmd, useRevisionIdHackmdSynced } from '~/stores/hackmd';
 import { useSWRxCurrentPage, useSWRxIsGrantNormalized } from '~/stores/page';
@@ -65,7 +63,7 @@ import {
 
 import { NextPageWithLayout } from './_app.page';
 import {
-  CommonProps, getNextI18NextConfig, getServerSideCommonProps, generateCustomTitleForPage,
+  CommonProps, getNextI18NextConfig, getServerSideCommonProps, generateCustomTitleForPage, useInitSidebarConfig,
 } from './utils/commons';
 
 
@@ -201,12 +199,8 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   useEditorConfig(props.editorConfig);
   useCsrfToken(props.csrfToken);
 
-  // UserUISettings
-  usePreferDrawerModeByUser(props.userUISettings?.preferDrawerModeByUser ?? props.sidebarConfig.isSidebarDrawerMode);
-  usePreferDrawerModeOnEditByUser(props.userUISettings?.preferDrawerModeOnEditByUser);
-  useSidebarCollapsed(props.userUISettings?.isSidebarCollapsed ?? props.sidebarConfig.isSidebarClosedAtDockMode);
-  useCurrentSidebarContents(props.userUISettings?.currentSidebarContents);
-  useCurrentProductNavWidth(props.userUISettings?.currentProductNavWidth);
+  // init sidebar config with UserUISettings and sidebarConfig
+  useInitSidebarConfig(props.sidebarConfig, props.userUISettings);
 
   // page
   useIsLatestRevision(props.isLatestRevision);
@@ -270,7 +264,7 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   useSetupGlobalSocket();
   useSetupGlobalSocketForPage(pageId);
 
-  const growiLayoutFluidClass = useCurrentGrowiLayoutFluidClassName();
+  const growiLayoutFluidClass = useCurrentGrowiLayoutFluidClassName(pageWithMeta?.data);
 
   const shouldRenderPutbackPageModal = pageWithMeta != null
     ? _isTrashPage(pageWithMeta.data.path)
@@ -298,10 +292,6 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
 
   const title = generateCustomTitleForPage(props, pagePath);
 
-  // TODO: show SSR body
-  // const rendererOptions = generateSSRViewOptions(props.rendererConfig, pagePath);
-  // const ssrBody = <RevisionRenderer rendererOptions={rendererOptions} markdown={revisionBody ?? ''} />;
-
   return (
     <>
       <Head>
@@ -325,9 +315,8 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
           pageView={
             <PageView
               pagePath={pagePath}
-              page={pageWithMeta?.data}
-              // TODO: show SSR body
-              // ssrBody={ssrBody}
+              initialPage={pageWithMeta?.data}
+              rendererConfig={props.rendererConfig}
             />
           }
         />

+ 3 - 7
packages/app/src/pages/_private-legacy-pages.page.tsx

@@ -23,7 +23,7 @@ import {
 } from '~/stores/ui';
 
 import {
-  CommonProps, getNextI18NextConfig, getServerSideCommonProps, generateCustomTitle,
+  CommonProps, getNextI18NextConfig, getServerSideCommonProps, generateCustomTitle, useInitSidebarConfig,
 } from './utils/commons';
 
 const SearchResultLayout = dynamic(() => import('~/components/Layout/SearchResultLayout'), { ssr: false });
@@ -67,12 +67,8 @@ const PrivateLegacyPage: NextPage<Props> = (props: Props) => {
 
   useDrawioUri(props.drawioUri);
 
-  // UserUISettings
-  usePreferDrawerModeByUser(userUISettings?.preferDrawerModeByUser ?? props.sidebarConfig.isSidebarDrawerMode);
-  usePreferDrawerModeOnEditByUser(userUISettings?.preferDrawerModeOnEditByUser);
-  useSidebarCollapsed(userUISettings?.isSidebarCollapsed ?? props.sidebarConfig.isSidebarClosedAtDockMode);
-  useCurrentSidebarContents(userUISettings?.currentSidebarContents);
-  useCurrentProductNavWidth(userUISettings?.currentProductNavWidth);
+  // init sidebar config with UserUISettings and sidebarConfig
+  useInitSidebarConfig(props.sidebarConfig, props.userUISettings);
 
   // render config
   useRendererConfig(props.rendererConfig);

+ 6 - 12
packages/app/src/pages/_search.page.tsx

@@ -1,11 +1,9 @@
-import {
-  NextPage, GetServerSideProps, GetServerSidePropsContext,
-} from 'next';
+import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
+import { useTranslation } from 'next-i18next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
 
-import { useTranslation } from 'next-i18next';
 import SearchResultLayout from '~/components/Layout/SearchResultLayout';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
@@ -25,9 +23,9 @@ import {
 
 import { SearchPage } from '../components/SearchPage';
 
-import { NextPageWithLayout } from './_app.page';
+import type { NextPageWithLayout } from './_app.page';
 import {
-  CommonProps, getNextI18NextConfig, getServerSideCommonProps, generateCustomTitle,
+  getNextI18NextConfig, getServerSideCommonProps, generateCustomTitle, CommonProps, useInitSidebarConfig,
 } from './utils/commons';
 
 
@@ -73,12 +71,8 @@ const SearchResultPage: NextPageWithLayout<Props> = (props: Props) => {
 
   useDrawioUri(props.drawioUri);
 
-  // UserUISettings
-  usePreferDrawerModeByUser(userUISettings?.preferDrawerModeByUser ?? props.sidebarConfig.isSidebarDrawerMode);
-  usePreferDrawerModeOnEditByUser(userUISettings?.preferDrawerModeOnEditByUser);
-  useSidebarCollapsed(userUISettings?.isSidebarCollapsed ?? props.sidebarConfig.isSidebarClosedAtDockMode);
-  useCurrentSidebarContents(userUISettings?.currentSidebarContents);
-  useCurrentProductNavWidth(userUISettings?.currentProductNavWidth);
+  // init sidebar config with UserUISettings and sidebarConfig
+  useInitSidebarConfig(props.sidebarConfig, props.userUISettings);
 
   // render config
   useRendererConfig(props.rendererConfig);

+ 3 - 7
packages/app/src/pages/me/[[...path]].page.tsx

@@ -30,7 +30,7 @@ import loggerFactory from '~/utils/logger';
 
 import { NextPageWithLayout } from '../_app.page';
 import {
-  CommonProps, getNextI18NextConfig, getServerSideCommonProps, generateCustomTitle,
+  CommonProps, getNextI18NextConfig, getServerSideCommonProps, generateCustomTitle, useInitSidebarConfig,
 } from '../utils/commons';
 
 
@@ -97,12 +97,8 @@ const MePage: NextPageWithLayout<Props> = (props: Props) => {
   // commons
   useCsrfToken(props.csrfToken);
 
-  // UserUISettings
-  usePreferDrawerModeByUser(props.userUISettings?.preferDrawerModeByUser ?? props.sidebarConfig.isSidebarDrawerMode);
-  usePreferDrawerModeOnEditByUser(props.userUISettings?.preferDrawerModeOnEditByUser);
-  useSidebarCollapsed(props.userUISettings?.isSidebarCollapsed ?? props.sidebarConfig.isSidebarClosedAtDockMode);
-  useCurrentSidebarContents(props.userUISettings?.currentSidebarContents);
-  useCurrentProductNavWidth(props.userUISettings?.currentProductNavWidth);
+  // init sidebar config with UserUISettings and sidebarConfig
+  useInitSidebarConfig(props.sidebarConfig, props.userUISettings);
 
   // page
   useIsSearchServiceConfigured(props.isSearchServiceConfigured);

+ 31 - 99
packages/app/src/pages/share/[[...path]].page.tsx

@@ -1,49 +1,40 @@
 import React from 'react';
 
-import { IUserHasId, IPagePopulatedToShowRevision } from '@growi/core';
-import {
+import type { IUserHasId, IPagePopulatedToShowRevision } from '@growi/core';
+import type {
   GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
-import dynamic from 'next/dynamic';
 import Head from 'next/head';
 import superjson from 'superjson';
 
 import { useCurrentGrowiLayoutFluidClassName } from '~/client/services/layout';
-import { MainPane } from '~/components/Layout/MainPane';
 import { ShareLinkLayout } from '~/components/Layout/ShareLinkLayout';
 import GrowiContextualSubNavigationSubstance from '~/components/Navbar/GrowiContextualSubNavigation';
-import RevisionRenderer from '~/components/Page/RevisionRenderer';
-import ShareLinkAlert from '~/components/Page/ShareLinkAlert';
-import type { PageSideContentsProps } from '~/components/PageSideContents';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
-import type { ShareLinkPageContentsProps } from '~/components/ShareLink/ShareLinkPageContents';
+import { ShareLinkPageView } from '~/components/ShareLink/ShareLinkPageView';
 import { SupportedAction, SupportedActionType } from '~/interfaces/activity';
-import { CrowiRequest } from '~/interfaces/crowi-request';
-import { RendererConfig } from '~/interfaces/services/renderer';
-import { IShareLinkHasId } from '~/interfaces/share-link';
+import type { CrowiRequest } from '~/interfaces/crowi-request';
+import type { RendererConfig } from '~/interfaces/services/renderer';
+import type { IShareLinkHasId } from '~/interfaces/share-link';
 import type { PageDocument } from '~/server/models/page';
-import { generateSSRViewOptions } from '~/services/renderer/renderer';
 import {
-  useCurrentUser, useCurrentPageId, useRendererConfig, useIsSearchPage, useCurrentPathname,
+  useCurrentUser, useCurrentPageId, useRendererConfig, useIsSearchPage, useCurrentPathname, useIsNotFound,
   useShareLinkId, useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsSearchScopeChildrenAsDefault, useDrawioUri, useIsContainerFluid,
 } from '~/stores/context';
 import loggerFactory from '~/utils/logger';
 
-import { NextPageWithLayout } from '../_app.page';
+import type { NextPageWithLayout } from '../_app.page';
 import {
-  CommonProps, getServerSideCommonProps, generateCustomTitleForPage, getNextI18NextConfig,
+  getServerSideCommonProps, generateCustomTitleForPage, getNextI18NextConfig, CommonProps,
 } from '../utils/commons';
 
 const logger = loggerFactory('growi:next-page:share');
 
-const PageSideContents = dynamic<PageSideContentsProps>(() => import('~/components/PageSideContents').then(mod => mod.PageSideContents), { ssr: false });
-// const Comments = dynamic(() => import('~/components/Comments').then(mod => mod.Comments), { ssr: false });
-const ForbiddenPage = dynamic(() => import('~/components/ForbiddenPage'), { ssr: false });
-
 type Props = CommonProps & {
   shareLinkRelatedPage?: IShareLinkRelatedPage,
   shareLink?: IShareLinkHasId,
+  isNotFound: boolean,
   isExpired: boolean,
   disableLinkSharing: boolean,
   isSearchServiceConfigured: boolean,
@@ -73,16 +64,16 @@ superjson.registerCustom<IShareLinkRelatedPage, string>(
 // GrowiContextualSubNavigation for shared page
 // get page info from props not to send request 'GET /page' from client
 type GrowiContextualSubNavigationForSharedPageProps = {
-  currentPage?: IPagePopulatedToShowRevision,
+  page?: IPagePopulatedToShowRevision,
   isLinkSharingDisabled: boolean,
 }
 
 const GrowiContextualSubNavigationForSharedPage = (props: GrowiContextualSubNavigationForSharedPageProps): JSX.Element => {
-  const { currentPage, isLinkSharingDisabled } = props;
-  if (currentPage == null) { return <></> }
+  const { page, isLinkSharingDisabled } = props;
+
   return (
     <div data-testid="grw-contextual-sub-nav">
-      <GrowiContextualSubNavigationSubstance currentPage={currentPage} isLinkSharingDisabled={isLinkSharingDisabled}/>
+      <GrowiContextualSubNavigationSubstance currentPage={page} isLinkSharingDisabled={isLinkSharingDisabled}/>
     </div>
   );
 };
@@ -90,6 +81,7 @@ const GrowiContextualSubNavigationForSharedPage = (props: GrowiContextualSubNavi
 const SharedPage: NextPageWithLayout<Props> = (props: Props) => {
   useCurrentPathname(props.shareLink?.relatedPage.path);
   useIsSearchPage(false);
+  useIsNotFound(props.isNotFound);
   useShareLinkId(props.shareLink?._id);
   useCurrentPageId(props.shareLink?.relatedPage._id);
   useCurrentUser(props.currentUser);
@@ -101,45 +93,12 @@ const SharedPage: NextPageWithLayout<Props> = (props: Props) => {
   useIsContainerFluid(props.isContainerFluid);
 
 
-  const growiLayoutFluidClass = useCurrentGrowiLayoutFluidClassName();
-
-  const isNotFound = props.shareLink == null || props.shareLink.relatedPage == null || props.shareLink.relatedPage.isEmpty;
-  const isShowSharedPage = !props.disableLinkSharing && !isNotFound && !props.isExpired;
-  const shareLink = props.shareLink;
+  const growiLayoutFluidClass = useCurrentGrowiLayoutFluidClassName(props.shareLinkRelatedPage);
 
   const pagePath = props.shareLinkRelatedPage?.path ?? '';
-  const revisionBody = props.shareLinkRelatedPage?.revision.body;
 
   const title = generateCustomTitleForPage(props, pagePath);
 
-  // TODO: show SSR body
-  // const rendererOptions = generateSSRViewOptions(props.rendererConfig, pagePath);
-  // const ssrBody = <RevisionRenderer rendererOptions={rendererOptions} markdown={revisionBody ?? ''} />;
-
-  const sideContents = shareLink != null
-    ? <PageSideContents page={shareLink.relatedPage} />
-    : <></>;
-
-  // const footerContents = shareLink != null && isPopulated(shareLink.relatedPage.revision)
-  //   ? (
-  //     <>
-  //       <Comments pageId={shareLink._id} pagePath={shareLink.relatedPage.path} revision={shareLink.relatedPage.revision} />
-  //     </>
-  //   )
-  //   : <></>;
-
-  const contents = (() => {
-    const ShareLinkPageContents = dynamic<ShareLinkPageContentsProps>(
-      () => import('~/components/ShareLink/ShareLinkPageContents').then(mod => mod.ShareLinkPageContents),
-      {
-        ssr: false,
-        // TODO: show SSR body
-        // loading: () => ssrBody,
-      },
-    );
-    return <ShareLinkPageContents page={props.shareLinkRelatedPage} />;
-  })();
-
   return (
     <>
       <Head>
@@ -148,50 +107,19 @@ const SharedPage: NextPageWithLayout<Props> = (props: Props) => {
 
       <div className={`dynamic-layout-root ${growiLayoutFluidClass} h-100 d-flex flex-column justify-content-between`}>
         <header className="py-0 position-relative">
-          {isShowSharedPage
-          && <GrowiContextualSubNavigationForSharedPage currentPage={props.shareLinkRelatedPage} isLinkSharingDisabled={props.disableLinkSharing} />}
+          <GrowiContextualSubNavigationForSharedPage page={props.shareLinkRelatedPage} isLinkSharingDisabled={props.disableLinkSharing} />
         </header>
 
         <div id="grw-fav-sticky-trigger" className="sticky-top"></div>
 
-        <MainPane
-          sideContents={sideContents}
-          // footerContents={footerContents}
-        >
-          { props.disableLinkSharing && (
-            <div className="mt-4">
-              <ForbiddenPage isLinkSharingDisabled={props.disableLinkSharing} />
-            </div>
-          )}
-
-          { (isNotFound && !props.disableLinkSharing) && (
-            <div className="container-lg">
-              <h2 className="text-muted mt-4">
-                <i className="icon-ban" aria-hidden="true" />
-                <span> Page is not found</span>
-              </h2>
-            </div>
-          )}
-
-          { (props.isExpired && !props.disableLinkSharing && shareLink != null) && (
-            <div className="container-lg">
-              <ShareLinkAlert expiredAt={shareLink.expiredAt} createdAt={shareLink.createdAt} />
-              <h2 className="text-muted mt-4">
-                <i className="icon-ban" aria-hidden="true" />
-                <span> Page is expired</span>
-              </h2>
-            </div>
-          )}
-
-          {(isShowSharedPage && shareLink != null) && (
-            <>
-              <ShareLinkAlert expiredAt={shareLink.expiredAt} createdAt={shareLink.createdAt} />
-              <div className="mb-5">
-                { contents }
-              </div>
-            </>
-          )}
-        </MainPane>
+        <ShareLinkPageView
+          pagePath={pagePath}
+          rendererConfig={props.rendererConfig}
+          page={props.shareLinkRelatedPage}
+          shareLink={props.shareLink}
+          isExpired={props.isExpired}
+          disableLinkSharing={props.disableLinkSharing}
+        />
 
       </div>
     </>
@@ -210,7 +138,7 @@ SharedPage.getLayout = function getLayout(page) {
 function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): void {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
-  const { configManager, searchService, xssService } = crowi;
+  const { configManager, searchService } = crowi;
 
   props.disableLinkSharing = configManager.getConfig('crowi', 'security:disableLinkSharing');
 
@@ -288,7 +216,11 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
   try {
     const ShareLinkModel = crowi.model('ShareLink');
     const shareLink = await ShareLinkModel.findOne({ _id: params.linkId }).populate('relatedPage');
-    if (shareLink != null) {
+    if (shareLink == null) {
+      props.isNotFound = true;
+    }
+    else {
+      props.isNotFound = false;
       props.shareLinkRelatedPage = await shareLink.relatedPage.populateDataToShowRevision();
       props.isExpired = shareLink.isExpired();
       props.shareLink = shareLink.toObject();

+ 3 - 6
packages/app/src/pages/tags.page.tsx

@@ -27,7 +27,7 @@ import {
 
 import { NextPageWithLayout } from './_app.page';
 import {
-  CommonProps, getServerSideCommonProps, getNextI18NextConfig, generateCustomTitle,
+  CommonProps, getServerSideCommonProps, getNextI18NextConfig, generateCustomTitle, useInitSidebarConfig,
 } from './utils/commons';
 
 const PAGING_LIMIT = 10;
@@ -72,11 +72,8 @@ const TagPage: NextPageWithLayout<CommonProps> = (props: Props) => {
   useIsSearchServiceReachable(props.isSearchServiceReachable);
   useIsSearchScopeChildrenAsDefault(props.isSearchScopeChildrenAsDefault);
 
-  usePreferDrawerModeByUser(props.userUISettings?.preferDrawerModeByUser ?? props.sidebarConfig.isSidebarDrawerMode);
-  usePreferDrawerModeOnEditByUser(props.userUISettings?.preferDrawerModeOnEditByUser);
-  useSidebarCollapsed(props.userUISettings?.isSidebarCollapsed ?? props.sidebarConfig.isSidebarClosedAtDockMode);
-  useCurrentSidebarContents(props.userUISettings?.currentSidebarContents);
-  useCurrentProductNavWidth(props.userUISettings?.currentProductNavWidth);
+  // init sidebar config with UserUISettings and sidebarConfig
+  useInitSidebarConfig(props.sidebarConfig, props.userUISettings);
 
   useRendererConfig(props.rendererConfig);
 

+ 7 - 17
packages/app/src/pages/trash.page.tsx

@@ -1,17 +1,15 @@
 import React from 'react';
 
 import type { IUser, IUserHasId } from '@growi/core';
-import { GetServerSideProps, GetServerSidePropsContext } from 'next';
-import { useTranslation } from 'next-i18next';
+import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
 
-import { useCurrentGrowiLayoutFluidClassName } from '~/client/services/layout';
 import { GrowiSubNavigation } from '~/components/Navbar/GrowiSubNavigation';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
-import { ISidebarConfig } from '~/interfaces/sidebar-config';
+import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 import type { IUserUISettings } from '~/interfaces/user-ui-settings';
 import type { UserUISettingsModel } from '~/server/models/user-ui-settings';
 import {
@@ -25,9 +23,9 @@ import {
   useIsSearchScopeChildrenAsDefault, useIsSearchPage, useShowPageLimitationXL, useIsGuestUser, useRendererConfig,
 } from '../stores/context';
 
-import { NextPageWithLayout } from './_app.page';
+import type { NextPageWithLayout } from './_app.page';
 import {
-  CommonProps, getServerSideCommonProps, getNextI18NextConfig, generateCustomTitleForPage,
+  getServerSideCommonProps, getNextI18NextConfig, generateCustomTitleForPage, CommonProps, useInitSidebarConfig,
 } from './utils/commons';
 
 const TrashPageList = dynamic(() => import('~/components/TrashPageList').then(mod => mod.TrashPageList), { ssr: false });
@@ -60,24 +58,16 @@ const TrashPage: NextPageWithLayout<CommonProps> = (props: Props) => {
   useCurrentPageId(null);
   useCurrentPathname('/trash');
 
-  // UserUISettings
-  usePreferDrawerModeByUser(props.userUISettings?.preferDrawerModeByUser ?? props.sidebarConfig.isSidebarDrawerMode);
-  usePreferDrawerModeOnEditByUser(props.userUISettings?.preferDrawerModeOnEditByUser);
-  useSidebarCollapsed(props.userUISettings?.isSidebarCollapsed ?? props.sidebarConfig.isSidebarClosedAtDockMode);
-  useCurrentSidebarContents(props.userUISettings?.currentSidebarContents);
-  useCurrentProductNavWidth(props.userUISettings?.currentProductNavWidth);
+  // init sidebar config with UserUISettings and sidebarConfig
+  useInitSidebarConfig(props.sidebarConfig, props.userUISettings);
 
   useShowPageLimitationXL(props.showPageLimitationXL);
 
   useRendererConfig(props.rendererConfig);
 
-  const { t } = useTranslation();
-
   const { data: isDrawerMode } = useDrawerMode();
   const { data: isGuestUser } = useIsGuestUser();
 
-  const growiLayoutFluidClass = useCurrentGrowiLayoutFluidClassName();
-
   const title = generateCustomTitleForPage(props, '/trash');
 
   return (
@@ -85,7 +75,7 @@ const TrashPage: NextPageWithLayout<CommonProps> = (props: Props) => {
       <Head>
         <title>{title}</title>
       </Head>
-      <div className={`dynamic-layout-root ${growiLayoutFluidClass}`}>
+      <div className="dynamic-layout-root">
         <header className="py-0 position-relative">
           <GrowiSubNavigation
             pagePath="/trash"

+ 17 - 3
packages/app/src/pages/utils/commons.ts

@@ -1,13 +1,18 @@
-import type { ColorScheme, IUser, IUserHasId } from '@growi/core';
+import type { ColorScheme, IUserHasId } from '@growi/core';
 import {
   DevidedPagePath, Lang, AllLang,
 } from '@growi/core';
-import { GetServerSideProps, GetServerSidePropsContext } from 'next';
-import { SSRConfig, UserConfig } from 'next-i18next';
+import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
+import type { SSRConfig, UserConfig } from 'next-i18next';
 
 import * as nextI18NextConfig from '^/config/next-i18next.config';
 
 import type { CrowiRequest } from '~/interfaces/crowi-request';
+import type { ISidebarConfig } from '~/interfaces/sidebar-config';
+import type { IUserUISettings } from '~/interfaces/user-ui-settings';
+import {
+  useCurrentProductNavWidth, useCurrentSidebarContents, usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed,
+} from '~/stores/ui';
 
 export type CommonProps = {
   namespacesRequired: string[], // i18next
@@ -139,3 +144,12 @@ export const generateCustomTitleForPage = (props: CommonProps, pagePath: string)
     .replace('{{pagepath}}', pagePath)
     .replace('{{pagename}}', dPagePath.latter);
 };
+
+export const useInitSidebarConfig = (sidebarConfig: ISidebarConfig, userUISettings?: IUserUISettings): void => {
+  // UserUISettings
+  usePreferDrawerModeByUser(userUISettings?.preferDrawerModeByUser ?? sidebarConfig.isSidebarDrawerMode);
+  usePreferDrawerModeOnEditByUser(userUISettings?.preferDrawerModeOnEditByUser);
+  useSidebarCollapsed(userUISettings?.isSidebarCollapsed ?? sidebarConfig.isSidebarClosedAtDockMode);
+  useCurrentSidebarContents(userUISettings?.currentSidebarContents);
+  useCurrentProductNavWidth(userUISettings?.currentProductNavWidth);
+};

+ 15 - 5
packages/app/src/server/routes/apiv3/users.js

@@ -484,9 +484,11 @@ module.exports = (crowi) => {
       const userData = await User.findById(id);
       await userData.makeAdmin();
 
+      const serializedUserData = serializeUserSecurely(userData);
+
       activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_USERS_GIVE_ADMIN });
 
-      return res.apiv3({ userData });
+      return res.apiv3({ userData: serializedUserData });
     }
     catch (err) {
       logger.error('Error', err);
@@ -529,9 +531,11 @@ module.exports = (crowi) => {
       const userData = await User.findById(id);
       await userData.removeFromAdmin();
 
+      const serializedUserData = serializeUserSecurely(userData);
+
       activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_USERS_REMOVE_ADMIN });
 
-      return res.apiv3({ userData });
+      return res.apiv3({ userData: serializedUserData });
     }
     catch (err) {
       logger.error('Error', err);
@@ -581,9 +585,11 @@ module.exports = (crowi) => {
       const userData = await User.findById(id);
       await userData.statusActivate();
 
+      const serializedUserData = serializeUserSecurely(userData);
+
       activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_USERS_ACTIVATE });
 
-      return res.apiv3({ userData });
+      return res.apiv3({ userData: serializedUserData });
     }
     catch (err) {
       logger.error('Error', err);
@@ -625,9 +631,11 @@ module.exports = (crowi) => {
       const userData = await User.findById(id);
       await userData.statusSuspend();
 
+      const serializedUserData = serializeUserSecurely(userData);
+
       activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_USERS_DEACTIVATE });
 
-      return res.apiv3({ userData });
+      return res.apiv3({ userData: serializedUserData });
     }
     catch (err) {
       logger.error('Error', err);
@@ -672,9 +680,11 @@ module.exports = (crowi) => {
       await ExternalAccount.remove({ user: userData });
       await Page.removeByPath(`/user/${userData.username}`);
 
+      const serializedUserData = serializeUserSecurely(userData);
+
       activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_USERS_REMOVE });
 
-      return res.apiv3({ userData });
+      return res.apiv3({ userData: serializedUserData });
     }
     catch (err) {
       logger.error('Error', err);

+ 4 - 1
packages/app/src/services/renderer/rehype-plugins/relative-links.ts

@@ -17,6 +17,9 @@ const defaultHrefResolver: IHrefResolver = (relativeHref, basePath) => {
   return relativeUrl.pathname;
 };
 
+const isAnchorLink = (href: string): boolean => {
+  return href.toString().length > 0 && href[0] === '#';
+};
 
 export type RelativeLinksPluginParams = {
   pagePath?: string,
@@ -42,7 +45,7 @@ export const relativeLinks: Plugin<[RelativeLinksPluginParams]> = (options = {})
       }
 
       const href = anchor.properties.href;
-      if (href == null || typeof href !== 'string' || isAbsolute(href)) {
+      if (href == null || typeof href !== 'string' || isAbsolute(href) || isAnchorLink(href)) {
         return;
       }
 

+ 3 - 3
packages/app/src/services/renderer/rehype-plugins/relocate-toc.ts

@@ -1,6 +1,6 @@
-import rehypeToc, { HtmlElementNode } from 'rehype-toc';
-import { Plugin } from 'unified';
-import { Node } from 'unist';
+import rehypeToc, { type HtmlElementNode } from 'rehype-toc';
+import type { Plugin } from 'unified';
+import type { Node } from 'unist';
 
 type StoreTocPluginParams = {
   storeTocNode: (toc: HtmlElementNode) => void,

+ 13 - 8
packages/app/src/services/renderer/renderer.tsx

@@ -1,26 +1,26 @@
 // allow only types to import from react
-import { ComponentType } from 'react';
+import type { ComponentType } from 'react';
 
 import { isClient } from '@growi/core';
 import * as drawioPlugin from '@growi/remark-drawio';
 import growiDirective from '@growi/remark-growi-directive';
 import { Lsx, LsxImmutable } from '@growi/remark-lsx/components';
 import * as lsxGrowiPlugin from '@growi/remark-lsx/services/renderer';
-import { Schema as SanitizeOption } from 'hast-util-sanitize';
-import { SpecialComponents } from 'react-markdown/lib/ast-to-react';
-import { NormalComponents } from 'react-markdown/lib/complex-types';
-import { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
+import type { Schema as SanitizeOption } from 'hast-util-sanitize';
+import type { SpecialComponents } from 'react-markdown/lib/ast-to-react';
+import type { NormalComponents } from 'react-markdown/lib/complex-types';
+import type { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
 import katex from 'rehype-katex';
 import raw from 'rehype-raw';
 import sanitize, { defaultSchema as sanitizeDefaultSchema } from 'rehype-sanitize';
 import slug from 'rehype-slug';
-import { HtmlElementNode } from 'rehype-toc';
+import type { HtmlElementNode } from 'rehype-toc';
 import breaks from 'remark-breaks';
 import emoji from 'remark-emoji';
 import gfm from 'remark-gfm';
 import math from 'remark-math';
 import deepmerge from 'ts-deepmerge';
-import { PluggableList, Pluggable, PluginTuple } from 'unified';
+import type { PluggableList, Pluggable, PluginTuple } from 'unified';
 
 
 import { CodeBlock } from '~/components/ReactMarkdownComponents/CodeBlock';
@@ -30,7 +30,7 @@ import { NextLink } from '~/components/ReactMarkdownComponents/NextLink';
 import { Table } from '~/components/ReactMarkdownComponents/Table';
 import { TableWithEditButton } from '~/components/ReactMarkdownComponents/TableWithEditButton';
 import { RehypeSanitizeOption } from '~/interfaces/rehype';
-import { RendererConfig } from '~/interfaces/services/renderer';
+import type { RendererConfig } from '~/interfaces/services/renderer';
 import { registerGrowiFacade } from '~/utils/growi-facade';
 import loggerFactory from '~/utils/logger';
 
@@ -122,6 +122,10 @@ const generateCommonOptions = (pagePath: string|undefined): RendererOptions => {
       pukiwikiLikeLinker,
       growiDirective,
     ],
+    remarkRehypeOptions: {
+      clobberPrefix: 'mdcont-',
+      allowDangerousHtml: true,
+    },
     rehypePlugins: [
       [relativeLinksByPukiwikiLikeLinker, { pagePath }],
       [relativeLinks, { pagePath }],
@@ -324,6 +328,7 @@ export const generateSSRViewOptions = (
 
   // add rehype plugins
   rehypePlugins.push(
+    slug,
     [lsxGrowiPlugin.rehypePlugin, { pagePath, isSharedPage: config.isSharedPage }],
     rehypeSanitizePlugin,
     katex,

+ 27 - 21
packages/app/src/stores/renderer.tsx

@@ -1,13 +1,10 @@
-import {
-  useCallback, useEffect, useRef,
-} from 'react';
+import { useCallback } from 'react';
 
-import { HtmlElementNode } from 'rehype-toc';
-import useSWR, { SWRResponse } from 'swr';
-import useSWRImmutable from 'swr/immutable';
+import type { HtmlElementNode } from 'rehype-toc';
+import useSWR, { type SWRResponse } from 'swr';
 
 import {
-  RendererOptions,
+  type RendererOptions,
   generateSimpleViewOptions, generatePreviewOptions,
   generateViewOptions, generateTocOptions,
 } from '~/services/renderer/renderer';
@@ -26,17 +23,9 @@ export const useViewOptions = (): SWRResponse<RendererOptions, Error> => {
   const { data: rendererConfig } = useRendererConfig();
   const { mutate: mutateCurrentPageTocNode } = useCurrentPageTocNode();
 
-  const tocRef = useRef<HtmlElementNode|undefined>();
-
   const storeTocNodeHandler = useCallback((toc: HtmlElementNode) => {
-    tocRef.current = toc;
-  }, []);
-
-  useEffect(() => {
-    mutateCurrentPageTocNode(tocRef.current);
-  // using useRef not to re-render
-  // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [mutateCurrentPageTocNode, tocRef.current]);
+    mutateCurrentPageTocNode(toc, { revalidate: false });
+  }, [mutateCurrentPageTocNode]);
 
   const isAllDataValid = currentPagePath != null && rendererConfig != null;
 
@@ -50,7 +39,10 @@ export const useViewOptions = (): SWRResponse<RendererOptions, Error> => {
       return optionsGenerator(currentPagePath, rendererConfig, storeTocNodeHandler);
     },
     {
+      keepPreviousData: true,
       fallbackData: isAllDataValid ? generateViewOptions(currentPagePath, rendererConfig, storeTocNodeHandler) : undefined,
+      revalidateOnFocus: false,
+      revalidateOnReconnect: false,
     },
   );
 };
@@ -62,13 +54,16 @@ export const useTocOptions = (): SWRResponse<RendererOptions, Error> => {
 
   const isAllDataValid = currentPagePath != null && rendererConfig != null && tocNode != null;
 
-  return useSWRImmutable(
+  return useSWR(
     isAllDataValid
       ? ['tocOptions', currentPagePath, tocNode, rendererConfig]
       : null,
     ([, , tocNode, rendererConfig]) => generateTocOptions(rendererConfig, tocNode),
     {
+      keepPreviousData: true,
       fallbackData: isAllDataValid ? generateTocOptions(rendererConfig, tocNode) : undefined,
+      revalidateOnFocus: false,
+      revalidateOnReconnect: false,
     },
   );
 };
@@ -89,7 +84,10 @@ export const usePreviewOptions = (): SWRResponse<RendererOptions, Error> => {
       return optionsGenerator(rendererConfig, pagePath);
     },
     {
+      keepPreviousData: true,
       fallbackData: isAllDataValid ? generatePreviewOptions(rendererConfig, currentPagePath) : undefined,
+      revalidateOnFocus: false,
+      revalidateOnReconnect: false,
     },
   );
 };
@@ -100,7 +98,7 @@ export const useCommentForCurrentPageOptions = (): SWRResponse<RendererOptions,
 
   const isAllDataValid = currentPagePath != null && rendererConfig != null;
 
-  return useSWRImmutable(
+  return useSWR(
     isAllDataValid
       ? ['commentPreviewOptions', rendererConfig, currentPagePath]
       : null,
@@ -111,12 +109,15 @@ export const useCommentForCurrentPageOptions = (): SWRResponse<RendererOptions,
       rendererConfig.isEnabledLinebreaksInComments,
     ),
     {
+      keepPreviousData: true,
       fallbackData: isAllDataValid ? generateSimpleViewOptions(
         rendererConfig,
         currentPagePath,
         undefined,
         rendererConfig.isEnabledLinebreaksInComments,
       ) : undefined,
+      revalidateOnFocus: false,
+      revalidateOnReconnect: false,
     },
   );
 };
@@ -127,13 +128,15 @@ export const useSelectedPagePreviewOptions = (pagePath: string, highlightKeyword
 
   const isAllDataValid = rendererConfig != null;
 
-  return useSWRImmutable(
+  return useSWR(
     isAllDataValid
       ? ['selectedPagePreviewOptions', rendererConfig, pagePath, highlightKeywords]
       : null,
     ([, rendererConfig, pagePath, highlightKeywords]) => generateSimpleViewOptions(rendererConfig, pagePath, highlightKeywords),
     {
       fallbackData: isAllDataValid ? generateSimpleViewOptions(rendererConfig, pagePath, highlightKeywords) : undefined,
+      revalidateOnFocus: false,
+      revalidateOnReconnect: false,
     },
   );
 };
@@ -146,13 +149,16 @@ export const useCustomSidebarOptions = (): SWRResponse<RendererOptions, Error> =
 
   const isAllDataValid = rendererConfig != null;
 
-  return useSWRImmutable(
+  return useSWR(
     isAllDataValid
       ? ['customSidebarOptions', rendererConfig]
       : null,
     ([, rendererConfig]) => generateSimpleViewOptions(rendererConfig, '/'),
     {
+      keepPreviousData: true,
       fallbackData: isAllDataValid ? generateSimpleViewOptions(rendererConfig, '/') : undefined,
+      revalidateOnFocus: false,
+      revalidateOnReconnect: false,
     },
   );
 };

+ 9 - 9
packages/app/src/stores/ui.tsx

@@ -1,24 +1,24 @@
-import { RefObject, useCallback, useEffect } from 'react';
+import { type RefObject, useCallback, useEffect } from 'react';
 
 import {
-  isClient, isServer, pagePathUtils, Nullable, PageGrant,
+  isClient, isServer, pagePathUtils, type Nullable, PageGrant,
 } from '@growi/core';
-import { withUtils, SWRResponseWithUtils } from '@growi/core/src/utils/with-utils';
+import { withUtils, type SWRResponseWithUtils } from '@growi/core/src/utils/with-utils';
 import { Breakpoint, addBreakpointListener, cleanupBreakpointListener } from '@growi/ui';
-import { HtmlElementNode } from 'rehype-toc';
+import type { HtmlElementNode } from 'rehype-toc';
 import type SimpleBar from 'simplebar-react';
 import {
-  useSWRConfig, SWRResponse, Key,
+  useSWRConfig, type SWRResponse, type Key,
 } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 
-import { IFocusable } from '~/client/interfaces/focusable';
+import type { IFocusable } from '~/client/interfaces/focusable';
 import { useUserUISettings } from '~/client/services/user-ui-settings';
 import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
-import { IPageGrantData } from '~/interfaces/page';
-import { ISidebarConfig } from '~/interfaces/sidebar-config';
+import type { IPageGrantData } from '~/interfaces/page';
+import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 import { SidebarContentsType } from '~/interfaces/ui';
-import { UpdateDescCountData } from '~/interfaces/websocket';
+import type { UpdateDescCountData } from '~/interfaces/websocket';
 import loggerFactory from '~/utils/logger';
 
 import {

+ 0 - 3
packages/app/src/styles/_attachments.scss

@@ -1,3 +0,0 @@
-.attachment-userpicture .picture {
-  vertical-align: text-bottom;
-}

+ 0 - 27
packages/app/src/styles/_comment.scss

@@ -1,27 +0,0 @@
-// modal
-.page-comment-delete-modal .modal-content {
-  .modal-body {
-    .comment-body {
-      max-height: 13em;
-      // scrollable
-      overflow-y: auto;
-    }
-  }
-}
-
-
-.page-comments {
-  // TODO: Never use .page-comments-list-toggle-older class.
-  .page-comments-list-toggle-older {
-    display: inline-block;
-    font-size: 0.9em;
-  }
-  // TODO: "pointer-events: none;" moved to "Comment.module.scss" now.
-  // .page-comment was defined in _comment.scss and _comment_growi.scss
-  // Required if .page-comment is not under .growi but under .page-comments, or under .growi but not under .page-comments
-  .page-comment {
-    padding-top: 50px;
-    margin-top: -50px;
-    pointer-events: none;
-  }
-}

+ 0 - 20
packages/app/src/styles/_comment_growi.scss

@@ -1,20 +0,0 @@
-.growi {
-  // display cheatsheet for comment form only
-  .comment-form {
-    // TODO: Never use .editor-cheatsheet class.
-    .editor-cheatsheet {
-      display: none;
-    }
-
-    // textarea
-    // TODO: Never use .comment-form-comment class.
-    .comment-form-comment {
-      height: 80px;
-      &:focus,
-      &:not(:invalid) {
-        height: 180px;
-        transition: height 0.2s ease-out;
-      }
-    }
-  }
-}

+ 10 - 13
packages/app/src/styles/_editor.scss

@@ -219,6 +219,16 @@
       padding: 18px 15px 0;
       overflow-y: scroll;
     }
+    // editing /Sidebar
+    .page-editor-preview-body.preview-sidebar {
+      width: 320px;
+      margin-right: auto;
+      margin-left: auto;
+
+      .wiki {
+        @extend %grw-custom-sidebar-content;
+      }
+    }
 
     .grw-editor-configuration-dropdown {
       .icon-container {
@@ -232,19 +242,6 @@
 
   // .builtin-editor .tab-pane#edit
 
-  // editing /Sidebar
-  &.editing-sidebar {
-    .page-editor-preview-body {
-      width: 320px;
-      margin-right: auto;
-      margin-left: auto;
-
-      .wiki {
-        @extend %grw-custom-sidebar-content;
-      }
-    }
-  }
-
   &.hackmd {
     .hackmd-preinit,
     #iframe-hackmd-container > iframe {

+ 0 - 6
packages/app/src/styles/_me.scss

@@ -1,6 +0,0 @@
-.user-settings-page {
-  .title {
-    @include variable-font-size(28px);
-    line-height: 1.1em;
-  }
-}

+ 0 - 9
packages/app/src/styles/_old-ios.scss

@@ -1,9 +0,0 @@
-html[old-ios] .layout-root:not(.editing) {
-  .grw-navbar {
-    position: initial !important;
-    top: 0 !important;
-  }
-  .grw-subnav-fixed-container {
-    top: 0 !important;
-  }
-}

+ 0 - 9
packages/app/src/styles/_page-duplicate-modal.scss

@@ -1,9 +0,0 @@
-.grw-duplicate-page {
-  .duplicate-name {
-    list-style: none;
-  }
-
-  .duplicate-exist {
-    color: #c7254e;
-  }
-}

+ 0 - 20
packages/app/src/styles/_user.scss

@@ -1,20 +0,0 @@
-$easeInOutCubic: cubic-bezier(0.65, 0, 0.35, 1);
-
-%transitionForCompactMode {
-  // set transition-duration (normal -> compact)
-  transition: all 300ms $easeInOutCubic;
-}
-
-/*
- * Styles
- */
-
-.draft-list-item {
-  .icon-container {
-    .icon-copy,
-    .draft-delete,
-    .icon-edit {
-      cursor: pointer;
-    }
-  }
-}

+ 2 - 0
packages/app/src/styles/_variables.scss

@@ -38,3 +38,5 @@ $grw-logomark-width: 36px;
 $grw-nav-main-left-tab-width: 95px;
 $grw-nav-main-left-tab-width-mobile: 50px;
 $grw-nav-main-tab-height: 42px;
+
+$grw-scroll-margin-top-in-view: 130px;

+ 2 - 0
packages/app/src/styles/organisms/_wiki.scss

@@ -30,6 +30,8 @@
     &:first-child {
       margin-top: 0;
     }
+
+    scroll-margin-top: var.$grw-scroll-margin-top-in-view;
   }
 
   h1 {

+ 0 - 7
packages/app/src/styles/style-app.scss

@@ -40,23 +40,16 @@
 @import 'organisms/wiki';
 
 // // growi component
-// @import 'attachments';
-// @import 'comment';
-// @import 'comment_growi';
 // @import 'draft';
 @import 'editor';
 @import 'fonts';
 @import 'layout';
-// @import 'me';
 @import 'mirror_mode';
 @import 'modal';
-// @import 'old-ios';
-// @import 'page-duplicate-modal';
 @import 'page-path';
 @import 'search';
 @import 'tag';
 @import 'installer';
-// @import 'user';
 
 
 /*

+ 14 - 15
packages/app/src/styles/theme/_apply-colors.scss

@@ -496,32 +496,31 @@ ul.pagination {
  * GROWI Editor
  */
 .layout-root.editing {
-  .main {
-    background-color: hsl.darken(var(--bgcolor-global),2%);
+  background-color: hsl.darken(var(--bgcolor-global),2%);
 
+  &.builtin-editor {
     .page-editor-editor-container {
       border-right-color: var(--border-color-theme);
-
-      .navbar-editor {
-        background-color: var(--bgcolor-global); // same color with active tab
-        border-bottom-color: var(--border-color-theme);
-      }
     }
+  }
 
-    .page-editor-preview-container {
-      background-color: var(--bgcolor-global);
-    }
+  .navbar-editor {
+    background-color: var(--bgcolor-global); // same color with active tab
+    border-bottom-color: var(--border-color-theme);
+  }
+
+  .page-editor-preview-container {
+    background-color: var(--bgcolor-global);
   }
 }
 
+
 /*
  * Preview for editing /Sidebar
  */
-body.editing-sidebar {
-  .page-editor-preview-body {
-    color: var(--color-sidebar-context);
-    background-color: var(--bgcolor-sidebar-context);
-  }
+.page-editor-preview-body.preview-sidebar {
+  color: var(--color-sidebar-context);
+  background-color: var(--bgcolor-sidebar-context);
 }
 
 /*

+ 1 - 1
packages/remark-lsx/package.json

@@ -23,7 +23,7 @@
     "@growi/core": "^6.0.6-RC.0",
     "@growi/remark-growi-directive": "^6.0.6-RC.0",
     "@growi/ui": "^6.0.6-RC.0",
-    "swr": "^1.3.0"
+    "swr": "^2.0.3"
   },
   "devDependencies": {
     "eslint-plugin-regex": "^1.8.0",

+ 2 - 2
packages/remark-lsx/src/components/Lsx.tsx

@@ -37,9 +37,9 @@ const LsxSubstance = React.memo(({
     return new LsxContext(prefix, options);
   }, [depth, filter, num, prefix, reverse, sort, except]);
 
-  const { data, error } = useSWRxNodeTree(lsxContext, isImmutable);
+  const { data, error, isLoading: _isLoading } = useSWRxNodeTree(lsxContext, isImmutable);
 
-  const isLoading = data === undefined;
+  const isLoading = _isLoading || data === undefined;
   const hasError = error != null;
   const errorMessage = error?.message;
 

+ 9 - 7
packages/remark-lsx/src/stores/lsx.tsx

@@ -100,7 +100,7 @@ const useSWRxLsxResponse = (
 ): SWRResponse<LsxResponse, Error> => {
   return useSWR(
     ['/_api/lsx', pagePath, options, isImmutable],
-    (endpoint, pagePath, options) => {
+    ([endpoint, pagePath, options]) => {
       return axios.get(endpoint, {
         params: {
           pagePath,
@@ -109,6 +109,7 @@ const useSWRxLsxResponse = (
       }).then(result => result.data as LsxResponse);
     },
     {
+      keepPreviousData: true,
       revalidateIfStale: !isImmutable,
       revalidateOnFocus: !isImmutable,
       revalidateOnReconnect: !isImmutable,
@@ -122,22 +123,23 @@ type LsxNodeTree = {
 }
 
 export const useSWRxNodeTree = (lsxContext: LsxContext, isImmutable?: boolean): SWRResponse<LsxNodeTree, Error> => {
-  const { data, error } = useSWRxLsxResponse(lsxContext.pagePath, lsxContext.options, isImmutable);
-
-  const isLoading = data === undefined && error == null;
+  const {
+    data, error, isLoading, isValidating,
+  } = useSWRxLsxResponse(lsxContext.pagePath, lsxContext.options, isImmutable);
 
   return useSWR(
-    !isLoading ? ['lsxNodeTree', lsxContext.pagePath, lsxContext.options, isImmutable, data, error] : null,
-    (key, pagePath, options, isImmutable, data) => {
+    !isLoading && !isValidating ? ['lsxNodeTree', lsxContext.pagePath, lsxContext.options, isImmutable, data, error] : null,
+    ([, pagePath, , , data]) => {
       if (data === undefined || error != null) {
         throw error;
       }
       return {
-        nodeTree: generatePageNodeTree(pagePath, data.pages),
+        nodeTree: generatePageNodeTree(pagePath, data?.pages),
         toppageViewersCount: data.toppageViewersCount,
       };
     },
     {
+      keepPreviousData: true,
       revalidateIfStale: !isImmutable,
       revalidateOnFocus: !isImmutable,
       revalidateOnReconnect: !isImmutable,

+ 17 - 32
yarn.lock

@@ -4282,11 +4282,6 @@
   dependencies:
     "@types/unist" "*"
 
-"@types/mdurl@^1.0.0":
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-1.0.2.tgz#e2ce9d83a613bacf284c7be7d491945e39e1f8e9"
-  integrity sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==
-
 "@types/mime-types@^2.1.0":
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/@types/mime-types/-/mime-types-2.1.1.tgz#d9ba43490fa3a3df958759adf69396c3532cf2c1"
@@ -14440,18 +14435,15 @@ mdast-util-mdx-expression@^1.1.0:
     mdast-util-to-markdown "^1.0.0"
 
 mdast-util-to-hast@^12.1.0:
-  version "12.1.2"
-  resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-12.1.2.tgz#5c793b04014746585254c7ce0bc2d117201a5d1d"
-  integrity sha512-Wn6Mcj04qU4qUXHnHpPATYMH2Jd8RlntdnloDfYLe1ErWRHo6+pvSl/DzHp6sCZ9cBSYlc8Sk8pbwb8xtUoQhQ==
+  version "12.3.0"
+  resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-12.3.0.tgz#045d2825fb04374e59970f5b3f279b5700f6fb49"
+  integrity sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==
   dependencies:
     "@types/hast" "^2.0.0"
     "@types/mdast" "^3.0.0"
-    "@types/mdurl" "^1.0.0"
     mdast-util-definitions "^5.0.0"
-    mdurl "^1.0.0"
-    micromark-util-sanitize-uri "^1.0.0"
+    micromark-util-sanitize-uri "^1.1.0"
     trim-lines "^3.0.0"
-    unist-builder "^3.0.0"
     unist-util-generated "^2.0.0"
     unist-util-position "^4.0.0"
     unist-util-visit "^4.0.0"
@@ -14526,10 +14518,6 @@ mdast-util-wiki-link@^0.0.2:
     "@babel/runtime" "^7.12.1"
     mdast-util-to-markdown "^0.6.5"
 
-mdurl@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
-
 media-typer@0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
@@ -15013,6 +15001,15 @@ micromark-util-sanitize-uri@^1.0.0:
     micromark-util-encode "^1.0.0"
     micromark-util-symbol "^1.0.0"
 
+micromark-util-sanitize-uri@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.1.0.tgz#f12e07a85106b902645e0364feb07cf253a85aee"
+  integrity sha512-RoxtuSCX6sUNtxhbmsEFQfWzs8VN7cTctmBPvYivo98xb/kDEoTCtJQX5wyzIYEmk/lvNFTat4hL8oW0KndFpg==
+  dependencies:
+    micromark-util-character "^1.0.0"
+    micromark-util-encode "^1.0.0"
+    micromark-util-symbol "^1.0.0"
+
 micromark-util-subtokenize@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-1.0.2.tgz#ff6f1af6ac836f8bfdbf9b02f40431760ad89105"
@@ -21749,15 +21746,10 @@ swagger2openapi@^5.3.1:
     yaml "^1.3.1"
     yargs "^12.0.5"
 
-swr@^1.3.0:
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/swr/-/swr-1.3.0.tgz#c6531866a35b4db37b38b72c45a63171faf9f4e8"
-  integrity sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw==
-
-swr@^2.0.2:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/swr/-/swr-2.0.2.tgz#fd34f3aac354f6b70f9134eb4218c747cc899a8d"
-  integrity sha512-iHbQW17hsduonMEliZnr6/yaxb+yvLe2r0+AH+ZfeqKzwc2bb+QRYpZm5/b/H0Lxgy7VWow4o71JeSazSun+9A==
+swr@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/swr/-/swr-2.0.3.tgz#9fe59a17f55b0fdddccd76b7b2f723f9f8e2263e"
+  integrity sha512-sGvQDok/AHEWTPfhUWXEHBVEXmgGnuahyhmRQbjl9XBYxT/MSlAzvXEKQpyM++bMPaI52vcWS2HiKNaW7+9OFw==
   dependencies:
     use-sync-external-store "^1.2.0"
 
@@ -23069,13 +23061,6 @@ unique-string@^2.0.0:
   dependencies:
     crypto-random-string "^2.0.0"
 
-unist-builder@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/unist-builder/-/unist-builder-3.0.0.tgz#728baca4767c0e784e1e64bb44b5a5a753021a04"
-  integrity sha512-GFxmfEAa0vi9i5sd0R2kcrI9ks0r82NasRq5QHh2ysGngrc6GiqD5CDf1FjPenY4vApmFASBIIlk/jj5J5YbmQ==
-  dependencies:
-    "@types/unist" "^2.0.0"
-
 unist-types@^1.1.5:
   version "1.1.5"
   resolved "https://registry.yarnpkg.com/unist-types/-/unist-types-1.1.5.tgz#f001c0af2c3b0ac6e4f5d9aa114e7afe046d7abe"