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

Merge branch 'master' into fix/undeletable-trash-when-click-all-delete-button

jam411 3 лет назад
Родитель
Сommit
ec5ee33edc
80 измененных файлов с 1144 добавлено и 709 удалено
  1. 1 1
      .github/workflows/reusable-app-prod.yml
  2. 50 1
      CHANGELOG.md
  3. 1 1
      lerna.json
  4. 1 1
      package.json
  5. 1 1
      packages/app/docker/README.md
  6. 12 11
      packages/app/package.json
  7. 89 0
      packages/app/src/client/services/side-effects/drawio-modal-launcher-for-view.ts
  8. 33 0
      packages/app/src/client/services/side-effects/hackmd-draft-updated.ts
  9. 89 0
      packages/app/src/client/services/side-effects/handsontable-modal-launcher-for-view.ts
  10. 3 6
      packages/app/src/client/services/side-effects/hash-changed.ts
  11. 39 0
      packages/app/src/client/services/side-effects/page-updated.ts
  12. 11 5
      packages/app/src/components/Comments.tsx
  13. 37 0
      packages/app/src/components/Common/LazyRenderer.tsx
  14. 7 22
      packages/app/src/components/CustomNavigation/CustomTabContent.tsx
  15. 2 0
      packages/app/src/components/Layout/AdminLayout.tsx
  16. 1 1
      packages/app/src/components/Layout/MainPane.tsx
  17. 15 11
      packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  18. 7 2
      packages/app/src/components/Navbar/GrowiSubNavigationSwitcher.tsx
  19. 0 249
      packages/app/src/components/Page.tsx
  20. 30 132
      packages/app/src/components/Page/DisplaySwitcher.tsx
  21. 102 0
      packages/app/src/components/Page/PageContents.tsx
  22. 0 0
      packages/app/src/components/Page/PageView.module.scss
  23. 126 0
      packages/app/src/components/Page/PageView.tsx
  24. 1 1
      packages/app/src/components/Page/RevisionRenderer.tsx
  25. 0 1
      packages/app/src/components/PageEditor/CodeMirrorEditor.jsx
  26. 10 2
      packages/app/src/components/PageEditor/DrawioModal.tsx
  27. 22 17
      packages/app/src/components/PageEditor/LinkEditModal.jsx
  28. 4 1
      packages/app/src/components/PageRenameModal.tsx
  29. 11 2
      packages/app/src/components/ReactMarkdownComponents/Header.tsx
  30. 13 4
      packages/app/src/components/ReactMarkdownComponents/NextLink.tsx
  31. 4 0
      packages/app/src/components/SearchPage/SearchControl.tsx
  32. 11 0
      packages/app/src/components/SearchTypeahead.tsx
  33. 1 0
      packages/app/src/components/ShareLink/ShareLink.tsx
  34. 1 1
      packages/app/src/components/ShareLink/ShareLinkForm.tsx
  35. 22 20
      packages/app/src/components/ShareLink/ShareLinkList.tsx
  36. 64 0
      packages/app/src/components/ShareLink/ShareLinkPageContents.tsx
  37. 0 2
      packages/app/src/components/TableOfContents.tsx
  38. 14 3
      packages/app/src/components/TagCloudBox.tsx
  39. 4 1
      packages/app/src/components/TagList.tsx
  40. 33 70
      packages/app/src/pages/[[...path]].page.tsx
  41. 0 6
      packages/app/src/pages/_document.page.tsx
  42. 26 4
      packages/app/src/pages/share/[[...path]].page.tsx
  43. 20 8
      packages/app/src/server/middlewares/safe-redirect.ts
  44. 1 1
      packages/app/src/server/routes/apiv3/page.js
  45. 1 0
      packages/app/src/services/renderer/remark-plugins/pukiwiki-like-linker.ts
  46. 53 0
      packages/app/src/services/renderer/renderer.tsx
  47. 6 1
      packages/app/src/stores/editor.tsx
  48. 4 1
      packages/app/src/stores/renderer.tsx
  49. 3 4
      packages/app/src/stores/use-static-swr.tsx
  50. 28 50
      packages/app/src/styles/_mixins.scss
  51. 12 4
      packages/app/test/cypress/integration/20-basic-features/20-basic-features--click-page-icons.spec.ts
  52. 66 0
      packages/app/test/cypress/integration/22-sharelink/22-sharelink--access-to-sharelink.spec.ts
  53. 1 1
      packages/app/tsconfig.build.server.json
  54. 1 1
      packages/codemirror-textlint/package.json
  55. 1 1
      packages/core/package.json
  56. 4 0
      packages/core/src/test/util/page-path-utils.test.js
  57. 1 0
      packages/core/src/utils/page-path-utils.ts
  58. 1 1
      packages/hackmd/package.json
  59. 1 1
      packages/preset-themes/package.json
  60. 1 1
      packages/preset-themes/src/styles/antarctic.scss
  61. 2 2
      packages/preset-themes/src/styles/default.scss
  62. 1 1
      packages/preset-themes/src/styles/fire-red.scss
  63. 1 1
      packages/preset-themes/src/styles/future.scss
  64. 1 1
      packages/preset-themes/src/styles/halloween.scss
  65. 1 1
      packages/preset-themes/src/styles/island.scss
  66. 1 1
      packages/preset-themes/src/styles/jade-green.scss
  67. 1 1
      packages/preset-themes/src/styles/kibela.scss
  68. 1 1
      packages/preset-themes/src/styles/mono-blue.scss
  69. 1 1
      packages/preset-themes/src/styles/nature.scss
  70. 1 1
      packages/preset-themes/src/styles/wood.scss
  71. 1 1
      packages/remark-drawio/package.json
  72. 1 1
      packages/remark-growi-directive/package.json
  73. 9 12
      packages/remark-lsx/package.json
  74. 10 3
      packages/remark-lsx/src/services/renderer/lsx.ts
  75. 0 18
      packages/remark-lsx/tsconfig.build.esm.json
  76. 1 1
      packages/remark-lsx/tsconfig.build.json
  77. 1 1
      packages/slack/package.json
  78. 2 2
      packages/slackbot-proxy/package.json
  79. 2 2
      packages/ui/package.json
  80. 3 3
      yarn.lock

+ 1 - 1
.github/workflows/reusable-app-prod.yml

@@ -196,7 +196,7 @@ jobs:
       fail-fast: false
       matrix:
         # List string expressions that is comma separated ids of tests in "test/cypress/integration"
-        spec-group: ['10', '20', '21', '30', '40', '50', '60']
+        spec-group: ['10', '20', '21', '22', '30', '40', '50', '60']
 
     services:
       mongodb:

+ 50 - 1
CHANGELOG.md

@@ -1,9 +1,58 @@
 # Changelog
 
-## [Unreleased](https://github.com/weseek/growi/compare/v6.0.2...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v6.0.4...HEAD)
 
 *Please do not manually update this file. We've automated the process.*
 
+## [v6.0.4](https://github.com/weseek/growi/compare/v6.0.3...v6.0.4) - 2023-01-25
+
+### 🐛 Bug Fixes
+
+- fix: Invalid URL in markdown breaks browser (#7292) @yuki-takei
+- fix: Previous editing markdown remains after changing page (#7285) @yukendev
+
+### 🧰 Maintenance
+
+- ci(deps): bump ua-parser-js from 0.7.31 to 0.7.33 (#7293) @dependabot
+
+## [v6.0.3](https://github.com/weseek/growi/compare/v6.0.2...v6.0.3) - 2023-01-24
+
+### 💎 Features
+
+- feat: GROWI to GROWI transfer (#6727) @hakumizuki
+- feat: Use configured xss custom whitelist (#7252) @miya
+- imprv: UI admin g2g transfer advanced options (#7261) @hakumizuki
+
+### 🚀 Improvement
+
+- imprv: Do not retrieve page data using API in shared page (#7240) @miya
+- imprv: Use CSS variables (#7093) @yuki-takei
+- imprv: Do not request /pages.getPageTag when on a shared page (#7214) @miya
+
+### 🐛 Bug Fixes
+
+- fix: Share link management button is not available (#7286) @yuki-takei
+- fix: Color for blinked section (#7287) @ayaka0417
+- fix: Error toaster appears after renaming (#7276) @miya
+- fix: Search Tag From Tag Sidebar Correctly (#7282) @yukendev
+- fix: Ignore backslash in page path (#7284) @yuki-takei
+- fix: Bug in Page Tree Selected Item Background Color (#7272) @yukendev
+- fix: Type guard comment.createdAt (#7281) @hakumizuki
+- fix: Color of login form (#7275) @ayaka0417
+- fix: Body of shared page is not displayed (#7270) @miya
+- fix: Refactor uri decoding in getServerSideProps (#7268) @yukendev
+- fix: Cannot login with LDAP unless local strategy is enabled (#7259) @miya
+- fix: Skeleton color (#7264) @ayaka0417
+- fix: Refactor axios date serializer config (#7249) @yukendev
+- fix: DeletePageModal shows an incorrect label when open (#7224) @yukendev
+- fix: Page path is not displayed in browser tab on shared page (#7243) @miya
+- fix: Lsx encode prefix twice (#7239) @yuki-takei
+- fix: Initial value of the page grant respects the parent page's one (#7232) @yukendev
+
+### 🧰 Maintenance
+
+- support: Build container images with AWS CodeBuild (#7258) @yuki-takei
+
 ## [v6.0.2](https://github.com/weseek/growi/compare/v6.0.1...v6.0.2) - 2023-01-10
 
 ### 🐛 Bug Fixes

+ 1 - 1
lerna.json

@@ -1,7 +1,7 @@
 {
   "npmClient": "yarn",
   "useWorkspaces": true,
-  "version": "6.0.3-RC.0",
+  "version": "6.0.5-RC.0",
   "packages": [
     "packages/*"
   ]

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "6.0.3-RC.0",
+  "version": "6.0.5-RC.0",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",

+ 1 - 1
packages/app/docker/README.md

@@ -10,7 +10,7 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 
-* [`6.0.2`, `6.0`, `6`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v6.0.2/packages/app/docker/Dockerfile)
+* [`6.0.4`, `6.0`, `6`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v6.0.4/packages/app/docker/Dockerfile)
 * [`5.1.7`, `5.1`, `5`](https://github.com/weseek/growi/blob/v5.1.7/packages/app/docker/Dockerfile)
 * [`5.1.7-nocdn`, `5.1-nocdn`, `5-nocdn`](https://github.com/weseek/growi/blob/v5.1.7/packages/app/docker/Dockerfile)
 * [`4.5.23`, `4.5`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.23/packages/app/docker/Dockerfile)

+ 12 - 11
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "6.0.3-RC.0",
+  "version": "6.0.5-RC.0",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -55,7 +55,8 @@
     "escape-string-regexp": "5.0.0 or above exports only ESM",
     "next": "/Sandbox rendering is crashed with v12.3 or above ",
     "string-width": "5.0.0 or above exports only ESM.",
-    "prom-client": "!!DO NOT REMOVE!! A peer dependency of @promster."
+    "prom-client": "!!DO NOT REMOVE!! A peer dependency of @promster.",
+    "remark-wiki-link": "!!DO NOT REMOVE!! including 'mdast-util-wiki-link' and 'micromark-extension-wiki-link' required by pukiwiki-like-linker"
   },
   "dependencies": {
     "@akebifiky/remark-simple-plantuml": "^1.0.2",
@@ -66,14 +67,14 @@
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^6.0.3-RC.0",
-    "@growi/core": "^6.0.3-RC.0",
-    "@growi/hackmd": "^6.0.3-RC.0",
-    "@growi/preset-themes": "^6.0.3-RC.0",
-    "@growi/remark-drawio": "^6.0.3-RC.0",
-    "@growi/remark-growi-directive": "^6.0.3-RC.0",
-    "@growi/remark-lsx": "^6.0.3-RC.0",
-    "@growi/slack": "^6.0.3-RC.0",
+    "@growi/codemirror-textlint": "^6.0.5-RC.0",
+    "@growi/core": "^6.0.5-RC.0",
+    "@growi/hackmd": "^6.0.5-RC.0",
+    "@growi/preset-themes": "^6.0.5-RC.0",
+    "@growi/remark-drawio": "^6.0.5-RC.0",
+    "@growi/remark-growi-directive": "^6.0.5-RC.0",
+    "@growi/remark-lsx": "^6.0.5-RC.0",
+    "@growi/slack": "^6.0.5-RC.0",
     "@promster/express": "^7.0.6",
     "@promster/server": "^7.0.8",
     "@slack/web-api": "^6.2.4",
@@ -202,7 +203,7 @@
     "handsontable": "v7.0.0 or above is no loger MIT lisence."
   },
   "devDependencies": {
-    "@growi/ui": "^6.0.3-RC.0",
+    "@growi/ui": "^6.0.5-RC.0",
     "@handsontable/react": "=2.1.0",
     "@icon/themify-icons": "1.0.1-alpha.3",
     "@next/bundle-analyzer": "^12.2.3",

+ 89 - 0
packages/app/src/client/services/side-effects/drawio-modal-launcher-for-view.ts

@@ -0,0 +1,89 @@
+import { useCallback, useEffect } from 'react';
+
+import EventEmitter from 'events';
+
+import { DrawioEditByViewerProps } from '@growi/remark-drawio';
+
+import { useSaveOrUpdate } from '~/client/services/page-operation';
+import mdu from '~/components/PageEditor/MarkdownDrawioUtil';
+import type { OptionsToSave } from '~/interfaces/page-operation';
+import { useShareLinkId } from '~/stores/context';
+import { useDrawioModal } from '~/stores/modal';
+import { useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
+import loggerFactory from '~/utils/logger';
+
+
+const logger = loggerFactory('growi:cli:side-effects:useDrawioModalLauncherForView');
+
+
+declare global {
+  // eslint-disable-next-line vars-on-top, no-var
+  var globalEmitter: EventEmitter;
+}
+
+
+export const useDrawioModalLauncherForView = (opts?: {
+  onSaveSuccess?: (newMarkdown: string) => void,
+  onSaveError?: (error: any) => void,
+}): void => {
+
+  const { data: shareLinkId } = useShareLinkId();
+
+  const { data: currentPage } = useSWRxCurrentPage();
+  const { data: tagsInfo } = useSWRxTagsInfo(currentPage?._id);
+
+  const { open: openDrawioModal } = useDrawioModal();
+
+  const saveOrUpdate = useSaveOrUpdate();
+
+  const saveByDrawioModal = useCallback(async(drawioMxFile: string, bol: number, eol: number) => {
+    if (currentPage == null || tagsInfo == null || shareLinkId != null) {
+      return;
+    }
+
+    const currentMarkdown = currentPage.revision.body;
+    const newMarkdown = mdu.replaceDrawioInMarkdown(drawioMxFile, currentMarkdown, bol, eol);
+
+    const optionsToSave: OptionsToSave = {
+      isSlackEnabled: false,
+      slackChannels: '',
+      grant: currentPage.grant,
+      grantUserGroupId: currentPage.grantedGroup?._id,
+      grantUserGroupName: currentPage.grantedGroup?.name,
+      pageTags: tagsInfo.tags,
+    };
+
+    try {
+      const currentRevisionId = currentPage.revision._id;
+      await saveOrUpdate(
+        newMarkdown,
+        { pageId: currentPage._id, path: currentPage.path, revisionId: currentRevisionId },
+        optionsToSave,
+      );
+
+      opts?.onSaveSuccess?.(newMarkdown);
+    }
+    catch (error) {
+      logger.error('failed to save', error);
+      opts?.onSaveError?.(error);
+    }
+  }, [currentPage, opts, saveOrUpdate, shareLinkId, tagsInfo]);
+
+
+  // set handler to open DrawioModal
+  useEffect(() => {
+    // disable if share link
+    if (shareLinkId != null) {
+      return;
+    }
+
+    const handler = (data: DrawioEditByViewerProps) => {
+      openDrawioModal(data.drawioMxFile, drawioMxFile => saveByDrawioModal(drawioMxFile, data.bol, data.eol));
+    };
+    globalEmitter.on('launchDrawioModal', handler);
+
+    return function cleanup() {
+      globalEmitter.removeListener('launchDrawioModal', handler);
+    };
+  }, [openDrawioModal, saveByDrawioModal, shareLinkId]);
+};

+ 33 - 0
packages/app/src/client/services/side-effects/hackmd-draft-updated.ts

@@ -0,0 +1,33 @@
+import { useCallback, useEffect } from 'react';
+
+import { SocketEventName } from '~/interfaces/websocket';
+import { useCurrentPageId } from '~/stores/context';
+import { useIsHackmdDraftUpdatingInRealtime } from '~/stores/hackmd';
+import { useGlobalSocket } from '~/stores/websocket';
+
+export const useHackmdDraftUpdatedEffect = (): void => {
+
+  const { data: currentPageId } = useCurrentPageId();
+  const { mutate: mutateIsHackmdDraftUpdatingInRealtime } = useIsHackmdDraftUpdatingInRealtime();
+
+  const { data: socket } = useGlobalSocket();
+
+  const setIsHackmdDraftUpdatingInRealtime = useCallback((data) => {
+    const { s2cMessagePageUpdated } = data;
+    if (s2cMessagePageUpdated.pageId === currentPageId) {
+      mutateIsHackmdDraftUpdatingInRealtime(true);
+    }
+  }, [currentPageId, mutateIsHackmdDraftUpdatingInRealtime]);
+
+  // listen socket for hackmd saved
+  useEffect(() => {
+
+    if (socket == null) { return }
+
+    socket.on(SocketEventName.EditingWithHackmd, setIsHackmdDraftUpdatingInRealtime);
+
+    return () => {
+      socket.off(SocketEventName.EditingWithHackmd, setIsHackmdDraftUpdatingInRealtime);
+    };
+  }, [setIsHackmdDraftUpdatingInRealtime, socket]);
+};

+ 89 - 0
packages/app/src/client/services/side-effects/handsontable-modal-launcher-for-view.ts

@@ -0,0 +1,89 @@
+import { useCallback, useEffect } from 'react';
+
+import EventEmitter from 'events';
+
+import MarkdownTable from '~/client/models/MarkdownTable';
+import { useSaveOrUpdate } from '~/client/services/page-operation';
+import mtu from '~/components/PageEditor/MarkdownTableUtil';
+import type { OptionsToSave } from '~/interfaces/page-operation';
+import { useShareLinkId } from '~/stores/context';
+import { useHandsontableModal } from '~/stores/modal';
+import { useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
+import loggerFactory from '~/utils/logger';
+
+
+const logger = loggerFactory('growi:cli:side-effects:useHandsontableModalLauncherForView');
+
+
+declare global {
+  // eslint-disable-next-line vars-on-top, no-var
+  var globalEmitter: EventEmitter;
+}
+
+
+export const useHandsontableModalLauncherForView = (opts?: {
+  onSaveSuccess?: (newMarkdown: string) => void,
+  onSaveError?: (error: any) => void,
+}): void => {
+
+  const { data: shareLinkId } = useShareLinkId();
+
+  const { data: currentPage } = useSWRxCurrentPage();
+  const { data: tagsInfo } = useSWRxTagsInfo(currentPage?._id);
+
+  const { open: openHandsontableModal } = useHandsontableModal();
+
+  const saveOrUpdate = useSaveOrUpdate();
+
+  const saveByHandsontableModal = useCallback(async(table: MarkdownTable, bol: number, eol: number) => {
+    if (currentPage == null || tagsInfo == null || shareLinkId != null) {
+      return;
+    }
+
+    const currentMarkdown = currentPage.revision.body;
+    const newMarkdown = mtu.replaceMarkdownTableInMarkdown(table, currentMarkdown, bol, eol);
+
+    const optionsToSave: OptionsToSave = {
+      isSlackEnabled: false,
+      slackChannels: '',
+      grant: currentPage.grant,
+      grantUserGroupId: currentPage.grantedGroup?._id,
+      grantUserGroupName: currentPage.grantedGroup?.name,
+      pageTags: tagsInfo.tags,
+    };
+
+    try {
+      const currentRevisionId = currentPage.revision._id;
+      await saveOrUpdate(
+        newMarkdown,
+        { pageId: currentPage._id, path: currentPage.path, revisionId: currentRevisionId },
+        optionsToSave,
+      );
+
+      opts?.onSaveSuccess?.(newMarkdown);
+    }
+    catch (error) {
+      logger.error('failed to save', error);
+      opts?.onSaveError?.(error);
+    }
+  }, [currentPage, opts, saveOrUpdate, shareLinkId, tagsInfo]);
+
+
+  // set handler to open HandsonTableModal
+  useEffect(() => {
+    if (currentPage == null || shareLinkId != null) {
+      return;
+    }
+
+    const handler = (bol: number, eol: number) => {
+      const markdown = currentPage.revision.body;
+      const currentMarkdownTable = mtu.getMarkdownTableFromLine(markdown, bol, eol);
+      openHandsontableModal(currentMarkdownTable, undefined, false, table => saveByHandsontableModal(table, bol, eol));
+    };
+    globalEmitter.on('launchHandsonTableModal', handler);
+
+    return function cleanup() {
+      globalEmitter.removeListener('launchHandsonTableModal', handler);
+    };
+  }, [currentPage, openHandsontableModal, saveByHandsontableModal, shareLinkId]);
+};

+ 3 - 6
packages/app/src/components/EventListeneres/HashChanged.tsx → packages/app/src/client/services/side-effects/hash-changed.ts

@@ -1,4 +1,4 @@
-import React, { useCallback, useEffect } from 'react';
+import { useCallback, useEffect } from 'react';
 
 import { useRouter } from 'next/router';
 
@@ -8,8 +8,9 @@ import { useEditorMode, determineEditorModeByHash } from '~/stores/ui';
 /**
  * Change editorMode by browser forward/back operation
  */
-const HashChanged = (): JSX.Element => {
+export const useHashChangedEffect = (): void => {
   const router = useRouter();
+
   const { data: isEditable } = useIsEditable();
   const { data: editorMode, mutate: mutateEditorMode } = useEditorMode();
 
@@ -47,8 +48,4 @@ const HashChanged = (): JSX.Element => {
       router.events.off('routeChangeComplete', hashchangeHandler);
     };
   }, [hashchangeHandler, router.events]);
-
-  return <></>;
 };
-
-export default HashChanged;

+ 39 - 0
packages/app/src/client/services/side-effects/page-updated.ts

@@ -0,0 +1,39 @@
+import { useCallback, useEffect } from 'react';
+
+import { SocketEventName } from '~/interfaces/websocket';
+import { useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
+import { useGlobalSocket } from '~/stores/websocket';
+
+export const usePageUpdatedEffect = (): void => {
+
+  const { setRemoteLatestPageData } = useSetRemoteLatestPageData();
+
+  const { data: socket } = useGlobalSocket();
+
+  const setLatestRemotePageData = useCallback((data) => {
+    const { s2cMessagePageUpdated } = data;
+
+    const remoteData = {
+      remoteRevisionId: s2cMessagePageUpdated.revisionId,
+      remoteRevisionBody: s2cMessagePageUpdated.revisionBody,
+      remoteRevisionLastUpdateUser: s2cMessagePageUpdated.remoteLastUpdateUser,
+      remoteRevisionLastUpdatedAt: s2cMessagePageUpdated.revisionUpdateAt,
+      revisionIdHackmdSynced: s2cMessagePageUpdated.revisionIdHackmdSynced,
+      hasDraftOnHackmd: s2cMessagePageUpdated.hasDraftOnHackmd,
+    };
+    setRemoteLatestPageData(remoteData);
+  }, [setRemoteLatestPageData]);
+
+  // listen socket for someone updating this page
+  useEffect(() => {
+
+    if (socket == null) { return }
+
+    socket.on(SocketEventName.PageUpdated, setLatestRemotePageData);
+
+    return () => {
+      socket.off(SocketEventName.PageUpdated, setLatestRemotePageData);
+    };
+
+  }, [setLatestRemotePageData, socket]);
+};

+ 11 - 5
packages/app/src/components/Comments.tsx

@@ -1,20 +1,24 @@
 import React from 'react';
 
-import { IRevisionHasId } from '@growi/core';
+import { type IRevisionHasId, pagePathUtils } from '@growi/core';
 import dynamic from 'next/dynamic';
 
-import { PageCommentProps } from '~/components/PageComment';
+import type { PageCommentProps } from '~/components/PageComment';
 import { useSWRxPageComment } from '~/stores/comment';
 import { useIsTrashPage } from '~/stores/page';
 
 import { useCurrentUser } from '../stores/context';
 
-import { CommentEditorProps } from './PageComment/CommentEditor';
+import type { CommentEditorProps } from './PageComment/CommentEditor';
+
+
+const { isTopPage } = pagePathUtils;
+
 
 const PageComment = dynamic<PageCommentProps>(() => import('~/components/PageComment').then(mod => mod.PageComment), { ssr: false });
 const CommentEditor = dynamic<CommentEditorProps>(() => import('./PageComment/CommentEditor').then(mod => mod.CommentEditor), { ssr: false });
 
-type CommentsProps = {
+export type CommentsProps = {
   pageId: string,
   pagePath: string,
   revision: IRevisionHasId,
@@ -28,7 +32,9 @@ export const Comments = (props: CommentsProps): JSX.Element => {
   const { data: isDeleted } = useIsTrashPage();
   const { data: currentUser } = useCurrentUser();
 
-  if (pageId == null) {
+  const isTopPagePath = isTopPage(pagePath);
+
+  if (pageId == null || isTopPagePath) {
     return <></>;
   }
 

+ 37 - 0
packages/app/src/components/Common/LazyRenderer.tsx

@@ -0,0 +1,37 @@
+import React, { useEffect, useState } from 'react';
+
+type Props = {
+  shouldRender: boolean | (() => boolean),
+  children: JSX.Element,
+}
+
+export const LazyRenderer = (props: Props): JSX.Element => {
+  const { shouldRender: _shouldRender, children } = props;
+
+  const [isActivated, setActivated] = useState(false);
+
+  const shouldRender = typeof _shouldRender === 'function'
+    ? _shouldRender()
+    : _shouldRender;
+
+  useEffect(() => {
+    if (isActivated) {
+      return;
+    }
+    setActivated(shouldRender);
+  }, [isActivated, shouldRender]);
+
+  const additionalClassName = shouldRender ? '' : 'd-none';
+
+  if (!isActivated) {
+    return <></>;
+  }
+
+  return (
+    <>
+      { React.cloneElement(children, {
+        className: `${children.props.className ?? ''} ${additionalClassName}`,
+      }) }
+    </>
+  );
+};

+ 7 - 22
packages/app/src/components/CustomNavigation/CustomTabContent.tsx

@@ -1,41 +1,35 @@
-import React, { useEffect, useState } from 'react';
+import React from 'react';
 
-import PropTypes from 'prop-types';
 import {
   TabContent, TabPane,
 } from 'reactstrap';
 
-import { ICustomNavTabMappings } from '~/interfaces/ui';
+import type { ICustomNavTabMappings } from '~/interfaces/ui';
+
+import { LazyRenderer } from '../Common/LazyRenderer';
 
 
 type Props = {
   activeTab: string,
   navTabMapping: ICustomNavTabMappings,
   additionalClassNames?: string[],
-
 }
 
 const CustomTabContent = (props: Props): JSX.Element => {
 
   const { activeTab, navTabMapping, additionalClassNames } = props;
 
-  const [activatedContent, setActivatedContent] = useState(new Set([activeTab]));
-
-  // add activated content to Set
-  useEffect(() => {
-    setActivatedContent(activatedContent.add(activeTab));
-  }, [activatedContent, activeTab]);
-
   return (
     <TabContent activeTab={activeTab} className={additionalClassNames != null ? additionalClassNames.join(' ') : ''}>
       {Object.entries(navTabMapping).map(([key, value]) => {
 
-        const shouldRender = key === activeTab || activatedContent.has(key);
         const { Content } = value;
 
         return (
           <TabPane key={key} tabId={key}>
-            { shouldRender && <Content /> }
+            <LazyRenderer shouldRender={key === activeTab}>
+              <Content />
+            </LazyRenderer>
           </TabPane>
         );
       })}
@@ -44,13 +38,4 @@ const CustomTabContent = (props: Props): JSX.Element => {
 
 };
 
-CustomTabContent.propTypes = {
-  activeTab: PropTypes.string.isRequired,
-  navTabMapping: PropTypes.object.isRequired,
-  additionalClassNames: PropTypes.arrayOf(PropTypes.string),
-};
-CustomTabContent.defaultProps = {
-  additionalClassNames: [],
-};
-
 export default CustomTabContent;

+ 2 - 0
packages/app/src/components/Layout/AdminLayout.tsx

@@ -10,6 +10,7 @@ import styles from './Admin.module.scss';
 
 
 const AdminNavigation = dynamic(() => import('~/components/Admin/Common/AdminNavigation'), { ssr: false });
+const PageCreateModal = dynamic(() => import('../PageCreateModal'), { ssr: false });
 const SystemVersion = dynamic(() => import('../SystemVersion'), { ssr: false });
 const HotkeysManager = dynamic(() => import('../Hotkeys/HotkeysManager'), { ssr: false });
 
@@ -45,6 +46,7 @@ const AdminLayout = ({
           </div>
         </div>
 
+        <PageCreateModal />
         <SystemVersion />
       </div>
 

+ 1 - 1
packages/app/src/components/Layout/MainPane.tsx

@@ -20,7 +20,7 @@ export const MainPane = (props: Props): JSX.Element => {
           <div id="content-main" className="content-main grw-container-convertible">
             { sideContents != null
               ? (
-                <div className="d-flex flex-column flex-lg-row">
+                <div className="d-flex flex-column flex-column-reverse flex-lg-row">
                   <div className="flex-grow-1 flex-basis-0 mw-0">
                     {children}
                   </div>

+ 15 - 11
packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -35,6 +35,7 @@ import AttachmentIcon from '../Icons/AttachmentIcon';
 import HistoryIcon from '../Icons/HistoryIcon';
 import PresentationIcon from '../Icons/PresentationIcon';
 import ShareLinkIcon from '../Icons/ShareLinkIcon';
+import { NotAvailable } from '../NotAvailable';
 import { NotAvailableForNow } from '../NotAvailableForNow';
 import { Skeleton } from '../Skeleton';
 
@@ -140,17 +141,20 @@ const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element
         {t('attachment_data')}
       </DropdownItem>
 
-      <DropdownItem
-        onClick={() => openAccessoriesModal(PageAccessoriesModalContents.ShareLink)}
-        disabled={isGuestUser || isSharedUser || isLinkSharingDisabled}
-        data-testid="open-page-accessories-modal-btn-with-share-link-management-data-tab"
-        className="grw-page-control-dropdown-item"
-      >
-        <span className="grw-page-control-dropdown-icon">
-          <ShareLinkIcon />
-        </span>
-        {t('share_links.share_link_management')}
-      </DropdownItem>
+      { !isGuestUser && !isSharedUser && (
+        <NotAvailable isDisabled={isLinkSharingDisabled ?? false} title="Disabled by admin">
+          <DropdownItem
+            onClick={() => openAccessoriesModal(PageAccessoriesModalContents.ShareLink)}
+            data-testid="open-page-accessories-modal-btn-with-share-link-management-data-tab"
+            className="grw-page-control-dropdown-item"
+          >
+            <span className="grw-page-control-dropdown-icon">
+              <ShareLinkIcon />
+            </span>
+            {t('share_links.share_link_management')}
+          </DropdownItem>
+        </NotAvailable>
+      ) }
     </>
   );
 };

+ 7 - 2
packages/app/src/components/Navbar/GrowiSubNavigationSwitcher.tsx

@@ -15,6 +15,10 @@ import styles from './GrowiSubNavigationSwitcher.module.scss';
 
 const logger = loggerFactory('growi:cli:GrowiSubNavigationSticky');
 
+export type GrowiSubNavigationSwitcherProps = {
+  isLinkSharingDisabled: boolean,
+}
+
 /**
  * GrowiSubNavigation
  *
@@ -22,7 +26,8 @@ const logger = loggerFactory('growi:cli:GrowiSubNavigationSticky');
  *   #grw-subnav-fixed-container element
  *   #grw-subnav-sticky-trigger element
  */
-export const GrowiSubNavigationSwitcher = (): JSX.Element => {
+export const GrowiSubNavigationSwitcher = (props: GrowiSubNavigationSwitcherProps): JSX.Element => {
+  const { isLinkSharingDisabled } = props;
 
   const { data: currentPage } = useSWRxCurrentPage();
   const { data: isSidebarCollapsed } = useSidebarCollapsed();
@@ -96,7 +101,7 @@ export const GrowiSubNavigationSwitcher = (): JSX.Element => {
         ref={fixedContainerRef}
         style={{ width }}
       >
-        <GrowiContextualSubNavigation currentPage={currentPage} isCompactMode isLinkSharingDisabled />
+        <GrowiContextualSubNavigation currentPage={currentPage} isCompactMode isLinkSharingDisabled={isLinkSharingDisabled} />
       </div>
     </div>
   );

+ 0 - 249
packages/app/src/components/Page.tsx

@@ -1,249 +0,0 @@
-import React, {
-  FC, useCallback,
-  useEffect, useRef,
-} from 'react';
-
-import EventEmitter from 'events';
-
-import { pagePathUtils, IPagePopulatedToShowRevision } from '@growi/core';
-import { DrawioEditByViewerProps } from '@growi/remark-drawio';
-import { useTranslation } from 'next-i18next';
-import dynamic from 'next/dynamic';
-import { HtmlElementNode } from 'rehype-toc';
-
-import MarkdownTable from '~/client/models/MarkdownTable';
-import { useSaveOrUpdate } from '~/client/services/page-operation';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
-import { OptionsToSave } from '~/interfaces/page-operation';
-import {
-  useIsGuestUser, useShareLinkId, useCurrentPathname,
-} from '~/stores/context';
-import { useEditingMarkdown } from '~/stores/editor';
-import { useDrawioModal, useHandsontableModal } from '~/stores/modal';
-import { useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
-import { useViewOptions } from '~/stores/renderer';
-import {
-  useCurrentPageTocNode,
-  useIsMobile,
-} from '~/stores/ui';
-import { registerGrowiFacade } from '~/utils/growi-facade';
-import loggerFactory from '~/utils/logger';
-
-import RevisionRenderer from './Page/RevisionRenderer';
-import mdu from './PageEditor/MarkdownDrawioUtil';
-import mtu from './PageEditor/MarkdownTableUtil';
-
-import styles from './Page.module.scss';
-
-
-declare global {
-  // eslint-disable-next-line vars-on-top, no-var
-  var globalEmitter: EventEmitter;
-}
-
-// const DrawioModal = dynamic(() => import('./PageEditor/DrawioModal'), { ssr: false });
-const GridEditModal = dynamic(() => import('./PageEditor/GridEditModal'), { ssr: false });
-const LinkEditModal = dynamic(() => import('./PageEditor/LinkEditModal'), { ssr: false });
-
-
-const logger = loggerFactory('growi:Page');
-
-type Props = {
-  currentPage?: IPagePopulatedToShowRevision,
-}
-
-export const Page: FC<Props> = (props: Props) => {
-  const { t } = useTranslation();
-  const { currentPage } = props;
-
-  // Pass tocRef to generateViewOptions (=> rehypePlugin => customizeTOC) to call mutateCurrentPageTocNode when tocRef.current changes.
-  // The toc node passed by customizeTOC is assigned to tocRef.current.
-  const tocRef = useRef<HtmlElementNode>();
-
-  const storeTocNodeHandler = useCallback((toc: HtmlElementNode) => {
-    tocRef.current = toc;
-  }, []);
-
-  const { data: currentPathname } = useCurrentPathname();
-  const isSharedPage = pagePathUtils.isSharedPage(currentPathname ?? '');
-
-  const { data: shareLinkId } = useShareLinkId();
-  const { mutate: mutateCurrentPage } = useSWRxCurrentPage();
-  const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
-  const { data: tagsInfo } = useSWRxTagsInfo(currentPage?._id);
-  const { data: isGuestUser } = useIsGuestUser();
-  const { data: isMobile } = useIsMobile();
-  const { data: rendererOptions, mutate: mutateRendererOptions } = useViewOptions(storeTocNodeHandler);
-  const { mutate: mutateCurrentPageTocNode } = useCurrentPageTocNode();
-  const { open: openDrawioModal } = useDrawioModal();
-  const { open: openHandsontableModal } = useHandsontableModal();
-
-  const saveOrUpdate = useSaveOrUpdate();
-
-
-  // register to facade
-  useEffect(() => {
-    registerGrowiFacade({
-      markdownRenderer: {
-        optionsMutators: {
-          viewOptionsMutator: mutateRendererOptions,
-        },
-      },
-    });
-  }, [mutateRendererOptions]);
-
-  useEffect(() => {
-    mutateCurrentPageTocNode(tocRef.current);
-  // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [mutateCurrentPageTocNode, tocRef.current]); // include tocRef.current to call mutateCurrentPageTocNode when tocRef.current changes
-
-
-  // TODO: refactor commonize saveByDrawioModal and saveByHandsontableModal
-  const saveByDrawioModal = useCallback(async(drawioMxFile: string, bol: number, eol: number) => {
-    if (currentPage == null || tagsInfo == null) {
-      return;
-    }
-
-    // disable if share link
-    if (shareLinkId != null) {
-      return;
-    }
-
-    const currentMarkdown = currentPage.revision.body;
-    const optionsToSave: OptionsToSave = {
-      isSlackEnabled: false,
-      slackChannels: '',
-      grant: currentPage.grant,
-      grantUserGroupId: currentPage.grantedGroup?._id,
-      grantUserGroupName: currentPage.grantedGroup?.name,
-      pageTags: tagsInfo.tags,
-    };
-
-    const newMarkdown = mdu.replaceDrawioInMarkdown(drawioMxFile, currentMarkdown, bol, eol);
-
-    try {
-      const currentRevisionId = currentPage.revision._id;
-      await saveOrUpdate(
-        newMarkdown,
-        { pageId: currentPage._id, path: currentPage.path, revisionId: currentRevisionId },
-        optionsToSave,
-      );
-
-      toastSuccess(t('toaster.save_succeeded'));
-
-      // rerender
-      if (!isSharedPage) {
-        mutateCurrentPage();
-      }
-      mutateEditingMarkdown(newMarkdown);
-    }
-    catch (error) {
-      logger.error('failed to save', error);
-      toastError(error);
-    }
-  }, [currentPage, isSharedPage, mutateCurrentPage, mutateEditingMarkdown, saveOrUpdate, shareLinkId, t, tagsInfo]);
-
-  // set handler to open DrawioModal
-  useEffect(() => {
-    // disable if share link
-    if (shareLinkId != null) {
-      return;
-    }
-
-    const handler = (data: DrawioEditByViewerProps) => {
-      openDrawioModal(data.drawioMxFile, drawioMxFile => saveByDrawioModal(drawioMxFile, data.bol, data.eol));
-    };
-    globalEmitter.on('launchDrawioModal', handler);
-
-    return function cleanup() {
-      globalEmitter.removeListener('launchDrawioModal', handler);
-    };
-  }, [openDrawioModal, saveByDrawioModal, shareLinkId]);
-
-  const saveByHandsontableModal = useCallback(async(table: MarkdownTable, bol: number, eol: number) => {
-    if (currentPage == null || tagsInfo == null || shareLinkId != null) {
-      return;
-    }
-
-    const currentMarkdown = currentPage.revision.body;
-    const optionsToSave: OptionsToSave = {
-      isSlackEnabled: false,
-      slackChannels: '',
-      grant: currentPage.grant,
-      grantUserGroupId: currentPage.grantedGroup?._id,
-      grantUserGroupName: currentPage.grantedGroup?.name,
-      pageTags: tagsInfo.tags,
-    };
-
-    const newMarkdown = mtu.replaceMarkdownTableInMarkdown(table, currentMarkdown, bol, eol);
-
-    try {
-      const currentRevisionId = currentPage.revision._id;
-      await saveOrUpdate(
-        newMarkdown,
-        { pageId: currentPage._id, path: currentPage.path, revisionId: currentRevisionId },
-        optionsToSave,
-      );
-
-      toastSuccess(t('toaster.save_succeeded'));
-
-      // rerender
-      if (!isSharedPage) {
-        mutateCurrentPage();
-      }
-      mutateEditingMarkdown(newMarkdown);
-    }
-    catch (error) {
-      logger.error('failed to save', error);
-      toastError(error);
-    }
-  }, [currentPage, isSharedPage, mutateCurrentPage, mutateEditingMarkdown, saveOrUpdate, shareLinkId, t, tagsInfo]);
-
-  // set handler to open HandsonTableModal
-  useEffect(() => {
-    if (currentPage == null || shareLinkId != null) {
-      return;
-    }
-
-    const handler = (bol: number, eol: number) => {
-      const markdown = currentPage.revision.body;
-      const currentMarkdownTable = mtu.getMarkdownTableFromLine(markdown, bol, eol);
-      openHandsontableModal(currentMarkdownTable, undefined, false, table => saveByHandsontableModal(table, bol, eol));
-    };
-    globalEmitter.on('launchHandsonTableModal', handler);
-
-    return function cleanup() {
-      globalEmitter.removeListener('launchHandsonTableModal', handler);
-    };
-  }, [currentPage, openHandsontableModal, saveByHandsontableModal, shareLinkId]);
-
-  if (currentPage == null || isGuestUser == null || rendererOptions == null) {
-    const entries = Object.entries({
-      currentPage, isGuestUser, rendererOptions,
-    })
-      .map(([key, value]) => [key, value == null ? 'null' : undefined])
-      .filter(([, value]) => value != null);
-
-    logger.warn('Some of materials are missing.', Object.fromEntries(entries));
-    return null;
-  }
-
-  const { _id: revisionId, body: markdown } = currentPage.revision;
-
-  return (
-    <div className={`mb-5 ${isMobile ? `page-mobile ${styles['page-mobile']}` : ''}`}>
-
-      { revisionId != null && (
-        <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown} />
-      )}
-
-      { !isGuestUser && (
-        <>
-          <GridEditModal />
-          <LinkEditModal />
-        </>
-      )}
-    </div>
-  );
-
-};

+ 30 - 132
packages/app/src/components/Page/DisplaySwitcher.tsx

@@ -1,157 +1,55 @@
-import React, { useCallback, useEffect, useMemo } from 'react';
+import React from 'react';
 
-import { pagePathUtils } from '@growi/core';
 import dynamic from 'next/dynamic';
 
-import { SocketEventName } from '~/interfaces/websocket';
-import {
-  useIsEditable, useShareLinkId, useIsNotFound,
-} from '~/stores/context';
-import { useIsHackmdDraftUpdatingInRealtime } from '~/stores/hackmd';
-import { useCurrentPagePath, useSWRxCurrentPage } from '~/stores/page';
-import {
-  useSetRemoteLatestPageData,
-} from '~/stores/remote-latest-page';
-import { EditorMode, useEditorMode } from '~/stores/ui';
-import { useGlobalSocket } from '~/stores/websocket';
 
-import CustomTabContent from '../CustomNavigation/CustomTabContent';
-import { Page } from '../Page';
-import { UserInfoProps } from '../User/UserInfo';
+import { useHackmdDraftUpdatedEffect } from '~/client/services/side-effects/hackmd-draft-updated';
+import { useHashChangedEffect } from '~/client/services/side-effects/hash-changed';
+import { usePageUpdatedEffect } from '~/client/services/side-effects/page-updated';
+import { useIsEditable } from '~/stores/context';
+import { EditorMode, useEditorMode } from '~/stores/ui';
 
-const { isUsersHomePage } = pagePathUtils;
+import { LazyRenderer } from '../Common/LazyRenderer';
 
 
 const PageEditor = dynamic(() => import('../PageEditor'), { ssr: false });
 const PageEditorByHackmd = dynamic(() => import('../PageEditorByHackmd').then(mod => mod.PageEditorByHackmd), { ssr: false });
 const EditorNavbarBottom = dynamic(() => import('../PageEditor/EditorNavbarBottom'), { ssr: false });
-const HashChanged = dynamic(() => import('../EventListeneres/HashChanged'), { ssr: false });
-const NotFoundPage = dynamic(() => import('../NotFoundPage'), { ssr: false });
-const UserInfo = dynamic<UserInfoProps>(() => import('../User/UserInfo').then(mod => mod.UserInfo), { ssr: false });
-
-
-const PageView = React.memo((): JSX.Element => {
-  const { data: currentPagePath } = useCurrentPagePath();
-  const { data: shareLinkId } = useShareLinkId();
-  const { data: isNotFound } = useIsNotFound();
-  const { data: currentPage } = useSWRxCurrentPage();
-  const { setRemoteLatestPageData } = useSetRemoteLatestPageData();
-
-  const { mutate: mutateIsHackmdDraftUpdatingInRealtime } = useIsHackmdDraftUpdatingInRealtime();
-
-  const isUsersHomePagePath = isUsersHomePage(currentPagePath ?? '');
-
-  const { data: socket } = useGlobalSocket();
-
-  const setLatestRemotePageData = useCallback((data) => {
-    const { s2cMessagePageUpdated } = data;
-
-    const remoteData = {
-      remoteRevisionId: s2cMessagePageUpdated.revisionId,
-      remoteRevisionBody: s2cMessagePageUpdated.revisionBody,
-      remoteRevisionLastUpdateUser: s2cMessagePageUpdated.remoteLastUpdateUser,
-      remoteRevisionLastUpdatedAt: s2cMessagePageUpdated.revisionUpdateAt,
-      revisionIdHackmdSynced: s2cMessagePageUpdated.revisionIdHackmdSynced,
-      hasDraftOnHackmd: s2cMessagePageUpdated.hasDraftOnHackmd,
-    };
-    setRemoteLatestPageData(remoteData);
-  }, [setRemoteLatestPageData]);
-
-  const setIsHackmdDraftUpdatingInRealtime = useCallback((data) => {
-    const { s2cMessagePageUpdated } = data;
-    if (s2cMessagePageUpdated.pageId === currentPage?._id) {
-      mutateIsHackmdDraftUpdatingInRealtime(true);
-    }
-  }, [currentPage?._id, mutateIsHackmdDraftUpdatingInRealtime]);
-
-  // listen socket for someone updating this page
-  useEffect(() => {
-
-    if (socket == null) { return }
-
-    socket.on(SocketEventName.PageUpdated, setLatestRemotePageData);
-
-    return () => {
-      socket.off(SocketEventName.PageUpdated, setLatestRemotePageData);
-    };
-
-  }, [setLatestRemotePageData, socket]);
 
-  // listen socket for hackmd saved
-  useEffect(() => {
 
-    if (socket == null) { return }
+type Props = {
+  pageView: JSX.Element,
+}
 
-    socket.on(SocketEventName.EditingWithHackmd, setIsHackmdDraftUpdatingInRealtime);
-
-    return () => {
-      socket.off(SocketEventName.EditingWithHackmd, setIsHackmdDraftUpdatingInRealtime);
-    };
-  }, [setIsHackmdDraftUpdatingInRealtime, socket]);
-
-  return (
-    <>
-      { isUsersHomePagePath && <UserInfo author={currentPage?.creator} /> }
-      { !isNotFound && <Page currentPage={currentPage ?? undefined} /> }
-      { isNotFound && <NotFoundPage /> }
-    </>
-  );
-});
-PageView.displayName = 'PageView';
-
-
-const DisplaySwitcher = React.memo((): JSX.Element => {
+export const DisplaySwitcher = (props: Props): JSX.Element => {
+  const { pageView } = props;
 
+  const { data: editorMode = EditorMode.View } = useEditorMode();
   const { data: isEditable } = useIsEditable();
 
-  const { data: editorMode = EditorMode.View } = useEditorMode();
+  usePageUpdatedEffect();
+  useHashChangedEffect();
+  useHackmdDraftUpdatedEffect();
 
   const isViewMode = editorMode === EditorMode.View;
 
-  const navTabMapping = useMemo(() => {
-    return {
-      [EditorMode.View]: {
-        Content: () => (
-          <div data-testid="page-view" id="page-view">
-            <PageView />
-          </div>
-        ),
-      },
-      [EditorMode.Editor]: {
-        Content: () => (
-          isEditable
-            ? (
-              <div data-testid="page-editor" id="page-editor">
-                <PageEditor />
-              </div>
-            )
-            : <></>
-        ),
-      },
-      [EditorMode.HackMD]: {
-        Content: () => (
-          isEditable
-            ? (
-              <div id="page-editor-with-hackmd">
-                <PageEditorByHackmd />
-              </div>
-            )
-            : <></>
-        ),
-      },
-    };
-  }, [isEditable]);
-
-
   return (
     <>
-      <CustomTabContent activeTab={editorMode} navTabMapping={navTabMapping} />
+      { isViewMode && pageView }
+
+      <LazyRenderer shouldRender={isEditable === true && editorMode === EditorMode.Editor}>
+        <div data-testid="page-editor" id="page-editor" className="editor-root">
+          <PageEditor />
+        </div>
+      </LazyRenderer>
+
+      <LazyRenderer shouldRender={isEditable === true && editorMode === EditorMode.HackMD}>
+        <div id="page-editor-with-hackmd" className="editor-root">
+          <PageEditorByHackmd />
+        </div>
+      </LazyRenderer>
 
       { isEditable && !isViewMode && <EditorNavbarBottom /> }
-      { isEditable && <HashChanged></HashChanged> }
     </>
   );
-});
-DisplaySwitcher.displayName = 'DisplaySwitcher';
-
-export default DisplaySwitcher;
+};

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

@@ -0,0 +1,102 @@
+import React, { useEffect } from 'react';
+
+import { pagePathUtils } from '@growi/core';
+import { useTranslation } from 'next-i18next';
+import type { HtmlElementNode } from 'rehype-toc';
+
+import { useDrawioModalLauncherForView } from '~/client/services/side-effects/drawio-modal-launcher-for-view';
+import { useHandsontableModalLauncherForView } from '~/client/services/side-effects/handsontable-modal-launcher-for-view';
+import { toastSuccess, toastError } from '~/client/util/toastr';
+import { useCurrentPathname } from '~/stores/context';
+import { useEditingMarkdown } from '~/stores/editor';
+import { useSWRxCurrentPage } from '~/stores/page';
+import { useViewOptions } from '~/stores/renderer';
+import { useCurrentPageTocNode } from '~/stores/ui';
+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: currentPathname } = useCurrentPathname();
+  const isSharedPage = pagePathUtils.isSharedPage(currentPathname ?? '');
+
+  const { data: currentPage, mutate: mutateCurrentPage } = useSWRxCurrentPage();
+  const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
+  const { mutate: mutateCurrentPageTocNode } = useCurrentPageTocNode();
+
+  const { data: rendererOptions, mutate: mutateRendererOptions } = useViewOptions((toc: HtmlElementNode) => {
+    mutateCurrentPageTocNode(toc);
+  });
+
+  // register to facade
+  useEffect(() => {
+    registerGrowiFacade({
+      markdownRenderer: {
+        optionsMutators: {
+          viewOptionsMutator: mutateRendererOptions,
+        },
+      },
+    });
+  }, [mutateRendererOptions]);
+
+  useHandsontableModalLauncherForView({
+    onSaveSuccess: (newMarkdown) => {
+      toastSuccess(t('toaster.save_succeeded'));
+
+      // rerender
+      if (!isSharedPage) {
+        mutateCurrentPage();
+      }
+      mutateEditingMarkdown(newMarkdown);
+    },
+    onSaveError: (error) => {
+      toastError(error);
+    },
+  });
+
+  useDrawioModalLauncherForView({
+    onSaveSuccess: (newMarkdown) => {
+      toastSuccess(t('toaster.save_succeeded'));
+
+      // rerender
+      if (!isSharedPage) {
+        mutateCurrentPage();
+      }
+      mutateEditingMarkdown(newMarkdown);
+    },
+    onSaveError: (error) => {
+      toastError(error);
+    },
+  });
+
+
+  if (currentPage == null || rendererOptions == null) {
+    const entries = Object.entries({
+      currentPage, 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} />
+      )}
+    </>
+  );
+
+};

+ 0 - 0
packages/app/src/components/Page.module.scss → packages/app/src/components/Page/PageView.module.scss


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

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

+ 1 - 1
packages/app/src/components/Page/RevisionRenderer.tsx

@@ -2,7 +2,7 @@ import React from 'react';
 
 import ReactMarkdown from 'react-markdown';
 
-import { RendererOptions } from '~/services/renderer/renderer';
+import type { RendererOptions } from '~/services/renderer/renderer';
 import loggerFactory from '~/utils/logger';
 
 

+ 0 - 1
packages/app/src/components/PageEditor/CodeMirrorEditor.jsx

@@ -19,7 +19,6 @@ import { UncontrolledCodeMirror } from '../UncontrolledCodeMirror';
 
 import AbstractEditor from './AbstractEditor';
 import CommentMentionHelper from './CommentMentionHelper';
-import { DrawioModal } from './DrawioModal';
 import EditorIcon from './EditorIcon';
 import EmojiPicker from './EmojiPicker';
 import EmojiPickerHelper from './EmojiPickerHelper';

+ 10 - 2
packages/app/src/components/PageEditor/DrawioModal.tsx

@@ -9,14 +9,15 @@ import {
   ModalBody,
 } from 'reactstrap';
 
-
 import { getDiagramsNetLangCode } from '~/client/util/locale-utils';
 import { useDrawioUri } from '~/stores/context';
 import { useDrawioModal } from '~/stores/modal';
 import { usePersonalSettings } from '~/stores/personal-settings';
+import loggerFactory from '~/utils/logger';
 
 import { DrawioCommunicationHelper } from './DrawioCommunicationHelper';
 
+const logger = loggerFactory('growi:components:DrawioModal');
 
 const headerColor = '#334455';
 const fontFamily = "Lato, -apple-system, BlinkMacSystemFont, 'Hiragino Kaku Gothic ProN', Meiryo, sans-serif";
@@ -52,7 +53,14 @@ export const DrawioModal = (): JSX.Element => {
       return undefined;
     }
 
-    const url = new URL(drawioUri);
+    let url;
+    try {
+      url = new URL(drawioUri);
+    }
+    catch (err) {
+      logger.debug(err);
+      return undefined;
+    }
 
     // refs: https://desk.draw.io/support/solutions/articles/16000042546-what-url-parameters-are-supported-
     url.searchParams.append('spin', '1');

+ 22 - 17
packages/app/src/components/PageEditor/LinkEditModal.jsx

@@ -18,15 +18,19 @@ import validator from 'validator';
 import Linker from '~/client/models/Linker';
 import { apiv3Get } from '~/client/util/apiv3-client';
 import { useCurrentPagePath } from '~/stores/page';
+import loggerFactory from '~/utils/logger';
 
 import PagePreviewIcon from '../Icons/PagePreviewIcon';
 import SearchTypeahead from '../SearchTypeahead';
 
 import Preview from './Preview';
 
+
 import styles from './LinkEditPreview.module.scss';
 
 
+const logger = loggerFactory('growi:components:LinkEditModal');
+
 class LinkEditModal extends React.PureComponent {
 
   constructor(props) {
@@ -96,17 +100,24 @@ class LinkEditModal extends React.PureComponent {
     // create url from link, add dummy origin if link is not valid url.
     // ex-1. link = 'https://growi.org/' -> url = 'https://growi.org/' (case-1,4)
     // ex-2. link = 'hoge' -> url = 'http://example.com/hoge' (case-2,3,5)
-    const url = new URL(link, 'http://example.com');
-    const isUrl = url.origin !== 'http://example.com';
-
+    let isFqcn = false;
     let isUseRelativePath = false;
-    let reshapedLink = link;
+    let url;
+    try {
+      const url = new URL(link, 'http://example.com');
+      isFqcn = url.origin !== 'http://example.com';
+    }
+    catch (err) {
+      logger.debug(err);
+    }
 
-    // if case-1, reshapedLink becomes page path
-    reshapedLink = this.convertUrlToPathIfPageUrl(reshapedLink, url);
+    // case-1: when link is this growi's page url, return pathname only
+    let reshapedLink = url != null && url.origin === window.location.origin
+      ? decodeURIComponent(url.pathname)
+      : link;
 
     // case-3
-    if (!isUrl && !reshapedLink.startsWith('/') && reshapedLink !== '') {
+    if (!isFqcn && !reshapedLink.startsWith('/') && reshapedLink !== '') {
       isUseRelativePath = true;
       const rootPath = this.getRootPath(type);
       reshapedLink = path.resolve(rootPath, reshapedLink);
@@ -118,12 +129,6 @@ class LinkEditModal extends React.PureComponent {
     });
   }
 
-  // return path name of link if link is this growi page url, else return original link.
-  convertUrlToPathIfPageUrl(link, url) {
-    // when link is this growi's page url, url.origin === window.location.origin and return path name
-    return url.origin === window.location.origin ? decodeURI(url.pathname) : link;
-  }
-
   cancel() {
     this.hide();
   }
@@ -161,11 +166,11 @@ class LinkEditModal extends React.PureComponent {
     let previewError = '';
 
     if (path.startsWith('/')) {
-      const pathWithoutFragment = new URL(path, 'http://dummy').pathname;
-      const isPermanentLink = validator.isMongoId(pathWithoutFragment.slice(1));
-      const pageId = isPermanentLink ? pathWithoutFragment.slice(1) : null;
-
       try {
+        const pathWithoutFragment = new URL(path, 'http://dummy').pathname;
+        const isPermanentLink = validator.isMongoId(pathWithoutFragment.slice(1));
+        const pageId = isPermanentLink ? pathWithoutFragment.slice(1) : null;
+
         const { data } = await apiv3Get('/page', { path: pathWithoutFragment, page_id: pageId });
         const { page } = data;
         markdown = page.revision.body;

+ 4 - 1
packages/app/src/components/PageRenameModal.tsx

@@ -20,7 +20,6 @@ import DuplicatedPathsTable from './DuplicatedPathsTable';
 import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
 import PagePathAutoComplete from './PagePathAutoComplete';
 
-
 const isV5Compatible = (meta: unknown): boolean => {
   return isIPageInfoForEntity(meta) ? meta.isV5Compatible : true;
 };
@@ -139,6 +138,10 @@ const PageRenameModal = (): JSX.Element => {
       setExistingPaths(existPaths);
     }
     catch (err) {
+      // Do not toast in case of this error because debounce process may be executed after the renaming process is completed.
+      if (err.length === 1 && err[0].code === 'from-page-is-not-exist') {
+        return;
+      }
       setErrs(err);
       toastError(t('modal_rename.label.Failed to get exist path'));
     }

+ 11 - 2
packages/app/src/components/ReactMarkdownComponents/Header.tsx

@@ -6,12 +6,15 @@ import { useRouter } from 'next/router';
 import { Element } from 'react-markdown/lib/rehype-filter';
 
 import { useIsGuestUser, useIsSharedUser, useShareLinkId } from '~/stores/context';
+import loggerFactory from '~/utils/logger';
 
 import { NextLink } from './NextLink';
 
 import styles from './Header.module.scss';
 
 
+const logger = loggerFactory('growi:components:Header');
+
 declare global {
   // eslint-disable-next-line vars-on-top, no-var
   var globalEmitter: EventEmitter;
@@ -67,8 +70,14 @@ export const Header = (props: HeaderProps): JSX.Element => {
   const CustomTag = `h${level}` as keyof JSX.IntrinsicElements;
 
   const activateByHash = useCallback((url: string) => {
-    const hash = (new URL(url, 'https://example.com')).hash.slice(1);
-    setActive(hash === id);
+    try {
+      const hash = (new URL(url, 'https://example.com')).hash.slice(1);
+      setActive(hash === id);
+    }
+    catch (err) {
+      logger.debug(err);
+      setActive(false);
+    }
   }, [id]);
 
   // init

+ 13 - 4
packages/app/src/components/ReactMarkdownComponents/NextLink.tsx

@@ -3,16 +3,25 @@ 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';
+
+
+const logger = loggerFactory('growi:components:NextLink');
 
 const isAnchorLink = (href: string): boolean => {
   return href.toString().length > 0 && href[0] === '#';
 };
 
 const isExternalLink = (href: string, siteUrl: string | undefined): boolean => {
-  const baseUrl = new URL(siteUrl ?? 'https://example.com');
-  const hrefUrl = new URL(href, baseUrl);
-
-  return baseUrl.host !== hrefUrl.host;
+  try {
+    const baseUrl = new URL(siteUrl ?? 'https://example.com');
+    const hrefUrl = new URL(href, baseUrl);
+    return baseUrl.host !== hrefUrl.host;
+  }
+  catch (err) {
+    logger.debug(err);
+    return false;
+  }
 };
 
 type Props = Omit<LinkProps, 'href'> & {

+ 4 - 0
packages/app/src/components/SearchPage/SearchControl.tsx

@@ -66,6 +66,10 @@ const SearchControl: FC <Props> = React.memo((props: Props) => {
     invokeSearch();
   }, [invokeSearch]);
 
+  useEffect(() => {
+    setKeyword(initialSearchConditions.keyword ?? '');
+  }, [initialSearchConditions.keyword]);
+
   return (
     <div className="position-sticky sticky-top shadow-sm">
       <div className="grw-search-page-nav d-flex py-3 align-items-center">

+ 11 - 0
packages/app/src/components/SearchTypeahead.tsx

@@ -45,6 +45,7 @@ type Props = TypeaheadProps & {
 
 // see https://github.com/ericgio/react-bootstrap-typeahead/issues/266#issuecomment-414987723
 type TypeaheadInstance = {
+  setState(input: { text: string | undefined; }): void;
   clear: () => void,
   focus: () => void,
   toggleMenu: () => void,
@@ -164,6 +165,16 @@ const SearchTypeahead: ForwardRefRenderFunction<IFocusable, Props> = (props: Pro
     }
   }, [onSearchError, searchError]);
 
+  useEffect(() => {
+    // update input with Next Link
+    // update input workaround. see: https://github.com/ericgio/react-bootstrap-typeahead/issues/266#issuecomment-414987723
+    if (typeaheadRef.current != null) {
+      typeaheadRef.current.setState({
+        text: keywordOnInit,
+      });
+    }
+  }, [keywordOnInit]);
+
   const labelKey = useCallback((option?: IPageWithSearchMeta) => {
     return option?.data.path ?? '';
   }, []);

+ 1 - 0
packages/app/src/components/ShareLink/ShareLink.tsx

@@ -64,6 +64,7 @@ const ShareLink = (): JSX.Element => {
           className="btn btn-outline-secondary d-block mx-auto px-5"
           type="button"
           onClick={toggleShareLinkFormHandler}
+          data-testid="btn-sharelink-toggleform"
         >
           {isOpenShareLinkForm ? t('Close') : t('New')}
         </button>

+ 1 - 1
packages/app/src/components/ShareLink/ShareLinkForm.tsx

@@ -199,7 +199,7 @@ export const ShareLinkForm: FC<Props> = (props: Props) => {
             />
           </div>
         </div>
-        <button type="button" className="btn btn-primary d-block mx-auto px-5" onClick={handleIssueShareLink}>
+        <button type="button" className="btn btn-primary d-block mx-auto px-5" onClick={handleIssueShareLink} data-testid="btn-sharelink-issue">
           {t('share_links.Issue')}
         </button>
       </div>

+ 22 - 20
packages/app/src/components/ShareLink/ShareLinkList.tsx

@@ -24,21 +24,19 @@ const ShareLinkTr = (props: ShareLinkTrProps): JSX.Element => {
 
   return (
     <tr key={shareLinkId}>
-      <td>
-        <div className="d-flex">
-          <span className="mr-auto my-auto">{shareLinkId}</span>
-
-          { isRelatedPageExists && (
-            <CopyDropdown
-              pagePath={relatedPage.path}
-              dropdownToggleId={`copydropdown-${shareLinkId}`}
-              pageId={shareLinkId}
-              isShareLinkMode
-            >
-              Copy Link
-            </CopyDropdown>
-          ) }
-        </div>
+      <td className="d-flex justify-content-between align-items-center">
+        <span>{shareLinkId}</span>
+
+        { isRelatedPageExists && (
+          <CopyDropdown
+            pagePath={relatedPage.path}
+            dropdownToggleId={`copydropdown-${shareLinkId}`}
+            pageId={shareLinkId}
+            isShareLinkMode
+          >
+            Copy Link
+          </CopyDropdown>
+        ) }
       </td>
       { isAdmin && (
         <td>
@@ -48,9 +46,13 @@ const ShareLinkTr = (props: ShareLinkTrProps): JSX.Element => {
           }
         </td>
       ) }
-      <td>{shareLink.expiredAt && <span>{dateFnsFormat(new Date(shareLink.expiredAt), 'yyyy-MM-dd HH:mm')}</span>}</td>
-      <td>{shareLink.description}</td>
-      <td>
+      <td style={{ verticalAlign: 'middle' }}>
+        {shareLink.description}
+      </td>
+      <td style={{ verticalAlign: 'middle' }}>
+        {shareLink.expiredAt && <span >{dateFnsFormat(new Date(shareLink.expiredAt), 'yyyy-MM-dd HH:mm')}</span>}
+      </td>
+      <td style={{ maxWidth: '0', textAlign: 'center' }}>
         <button className="btn btn-outline-warning" type="button" onClick={onDelete}>
           <i className="icon-trash"></i>{t('Delete')}
         </button>
@@ -96,10 +98,10 @@ const ShareLinkList = (props: Props): JSX.Element => {
       <table className="table table-bordered">
         <thead>
           <tr>
-            <th>{t('share_links.Share Link', { ns: 'commons' })}</th>
+            <th style={{ width: '350px' }}>{t('share_links.Share Link', { ns: 'commons' })}</th>
             {props.isAdmin && <th>{t('share_links.Page Path', { ns: 'commons' })}</th>}
-            <th>{t('share_links.expire', { ns: 'commons' })}</th>
             <th>{t('share_links.description', { ns: 'commons' })}</th>
+            <th style={{ width: '150px' }}>{t('share_links.expire', { ns: 'commons' })}</th>
             <th></th>
           </tr>
         </thead>

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

@@ -0,0 +1,64 @@
+import React, { useEffect } from 'react';
+
+import type { IPagePopulatedToShowRevision } from '@growi/core';
+import type { HtmlElementNode } from 'rehype-toc';
+
+import { useViewOptions } from '~/stores/renderer';
+import { useCurrentPageTocNode } from '~/stores/ui';
+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 { mutate: mutateCurrentPageTocNode } = useCurrentPageTocNode();
+
+  const { data: rendererOptions, mutate: mutateRendererOptions } = useViewOptions((toc: HtmlElementNode) => {
+    mutateCurrentPageTocNode(toc);
+  });
+
+  // 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} />
+      )}
+    </>
+  );
+
+};

+ 0 - 2
packages/app/src/components/TableOfContents.tsx

@@ -21,8 +21,6 @@ const TableOfContents = (): JSX.Element => {
 
   const isUserPage = currentPagePath != null && _isUserPage(currentPagePath);
 
-  // const [tocHtml, setTocHtml] = useState('');
-
   const { data: rendererOptions } = useTocOptions();
 
   const calcViewHeight = useCallback(() => {

+ 14 - 3
packages/app/src/components/TagCloudBox.tsx

@@ -1,7 +1,10 @@
 import React, { FC, memo } from 'react';
 
+import Link from 'next/link';
+
 import { IDataTagCount } from '~/interfaces/tag';
 
+
 type Props = {
   tags:IDataTagCount[],
   minSize?: number,
@@ -22,10 +25,18 @@ const TagCloudBox: FC<Props> = memo((props:(Props & typeof defaultProps)) => {
 
   const tagElements = tags.map((tag:IDataTagCount) => {
     const tagNameFormat = (tag.name).length > maxTagTextLength ? `${(tag.name).slice(0, maxTagTextLength)}...` : tag.name;
+
+    const url = new URL('/_search', 'https://example.com');
+    url.searchParams.append('q', `tag:${tag.name}`);
+
     return (
-      <a key={tag.name} href={`/_search?q=tag:${tag.name}`} className="grw-tag-label badge badge-secondary mr-2">
-        {tagNameFormat}
-      </a>
+      <Link
+        key={tag.name} href={`${url.pathname}${url.search}`}
+      >
+        <a className="grw-tag-label badge badge-secondary mr-2">
+          {tagNameFormat}
+        </a>
+      </Link>
     );
   });
 

+ 4 - 1
packages/app/src/components/TagList.tsx

@@ -33,10 +33,13 @@ const TagList: FC<TagListProps> = (props:(TagListProps & typeof defaultProps)) =
     return tagData.map((tag:IDataTagCount, index:number) => {
       const tagListClasses: string = index === 0 ? 'list-group-item d-flex' : 'list-group-item d-flex border-top-0';
 
+      const url = new URL('/_search', 'https://example.com');
+      url.searchParams.append('q', `tag:${tag.name}`);
+
       return (
         <Link
           key={tag._id}
-          href={`/_search?q=tag:${encodeURIComponent(tag.name)}`}
+          href={`${url.pathname}${url.search}`}
         >
           <a
             className={tagListClasses}

+ 33 - 70
packages/app/src/pages/[[...path]].page.tsx

@@ -20,25 +20,19 @@ import { useRouter } from 'next/router';
 import superjson from 'superjson';
 
 import { useCurrentGrowiLayoutFluidClassName } from '~/client/services/layout';
-import { Comments } from '~/components/Comments';
-import { MainPane } from '~/components/Layout/MainPane';
-import { PageAlerts } from '~/components/PageAlert/PageAlerts';
-// import { useTranslation } from '~/i18n';
-import { PageContentFooter } from '~/components/PageContentFooter';
+import { PageView } from '~/components/Page/PageView';
+import RevisionRenderer from '~/components/Page/RevisionRenderer';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
-import { UsersHomePageFooterProps } from '~/components/UsersHomePageFooter';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
-// import { renderScriptTagByName, renderHighlightJsStyleTag } from '~/service/cdn-resources-loader';
-// import { useRendererSettings } from '~/stores/renderer';
-// import { EditorMode, useEditorMode, useIsMobile } from '~/stores/ui';
 import type { EditorConfig } from '~/interfaces/editor-settings';
-import { IPageGrantData } from '~/interfaces/page';
+import type { IPageGrantData } from '~/interfaces/page';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 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';
@@ -51,17 +45,11 @@ import {
 import { useSetupGlobalSocket, useSetupGlobalSocketForPage } from '~/stores/websocket';
 import loggerFactory from '~/utils/logger';
 
-// import { isUserPage, isTrashPage, isSharedPage } from '~/utils/path-utils';
-
-// import GrowiSubNavigation from '../client/js/components/Navbar/GrowiSubNavigation';
-// import GrowiSubNavigationSwitcher from '../client/js/components/Navbar/GrowiSubNavigationSwitcher';
 import { DescendantsPageListModal } from '../components/DescendantsPageListModal';
 import { BasicLayoutWithEditorMode } from '../components/Layout/BasicLayout';
 import GrowiContextualSubNavigationSubstance from '../components/Navbar/GrowiContextualSubNavigation';
-import DisplaySwitcher from '../components/Page/DisplaySwitcher';
-// import { serializeUserSecurely } from '../server/models/serializers/user-serializer';
-// import PageStatusAlert from '../client/js/components/PageStatusAlert';
-import type { PageSideContentsProps } from '../components/PageSideContents';
+import type { GrowiSubNavigationSwitcherProps } from '../components/Navbar/GrowiSubNavigationSwitcher';
+import { DisplaySwitcher } from '../components/Page/DisplaySwitcher';
 import {
   useCurrentUser,
   useIsLatestRevision,
@@ -87,14 +75,9 @@ declare global {
 }
 
 
-const NotCreatablePage = dynamic(() => import('../components/NotCreatablePage').then(mod => mod.NotCreatablePage), { ssr: false });
-const ForbiddenPage = dynamic(() => import('../components/ForbiddenPage'), { ssr: false });
 const UnsavedAlertDialog = dynamic(() => import('../components/UnsavedAlertDialog'), { ssr: false });
-const PageSideContents = dynamic<PageSideContentsProps>(() => import('../components/PageSideContents').then(mod => mod.PageSideContents), { ssr: false });
-const GrowiSubNavigationSwitcher = dynamic(() => import('../components/Navbar/GrowiSubNavigationSwitcher')
+const GrowiSubNavigationSwitcher = dynamic<GrowiSubNavigationSwitcherProps>(() => import('../components/Navbar/GrowiSubNavigationSwitcher')
   .then(mod => mod.GrowiSubNavigationSwitcher), { ssr: false });
-const UsersHomePageFooter = dynamic<UsersHomePageFooterProps>(() => import('../components/UsersHomePageFooter')
-  .then(mod => mod.UsersHomePageFooter), { ssr: false });
 const DrawioModal = dynamic(() => import('../components/PageEditor/DrawioModal').then(mod => mod.DrawioModal), { ssr: false });
 const HandsontableModal = dynamic(() => import('../components/PageEditor/HandsontableModal').then(mod => mod.HandsontableModal), { ssr: false });
 const PageStatusAlert = dynamic(() => import('../components/PageStatusAlert').then(mod => mod.PageStatusAlert), { ssr: false });
@@ -102,7 +85,7 @@ const PageStatusAlert = dynamic(() => import('../components/PageStatusAlert').th
 const logger = loggerFactory('growi:pages:all');
 
 const {
-  isPermalink: _isPermalink, isUsersHomePage, isTrashPage: _isTrashPage, isCreatablePage, isTopPage,
+  isPermalink: _isPermalink, isTrashPage: _isTrashPage, isCreatablePage,
 } = pagePathUtils;
 const { removeHeadingSlash } = pathUtils;
 
@@ -148,11 +131,6 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
   );
 };
 
-const IdenticalPathPage = (): JSX.Element => {
-  const IdenticalPathPage = dynamic(() => import('../components/IdenticalPathPage').then(mod => mod.IdenticalPathPage), { ssr: false });
-  return <IdenticalPathPage />;
-};
-
 const PutbackPageModal = (): JSX.Element => {
   const PutbackPageModal = dynamic(() => import('../components/PutbackPageModal'), { ssr: false });
   return <PutbackPageModal />;
@@ -274,7 +252,8 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   const { pageWithMeta, userUISettings } = props;
 
   const pageId = pageWithMeta?.data._id;
-  const pagePath = pageWithMeta?.data.path ?? (!_isPermalink(props.currentPathname) ? props.currentPathname : undefined);
+  const pagePath = pageWithMeta?.data.path ?? props.currentPathname;
+  const revisionBody = pageWithMeta?.data.revision?.body;
 
   useCurrentPageId(pageId ?? null);
   useRevisionIdHackmdSynced(pageWithMeta?.data.revisionHackmdSynced);
@@ -285,7 +264,7 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
 
   useSWRxCurrentPage(pageWithMeta?.data ?? null); // store initial data
 
-  useEditingMarkdown(pageWithMeta?.data.revision?.body);
+  const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
 
   const { data: grantData } = useSWRxIsGrantNormalized(pageId);
   const { mutate: mutateSelectedGrant } = useSelectedGrant();
@@ -314,30 +293,16 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
     }
   }, [props.currentPathname, router]);
 
-  const isTopPagePath = isTopPage(pageWithMeta?.data.path ?? '');
-
-  const title = generateCustomTitleForPage(props, pagePath ?? '');
-
+  // initialize mutateEditingMarkdown only once per page
+  useEffect(() => {
+    mutateEditingMarkdown(revisionBody);
+  }, [mutateEditingMarkdown, revisionBody]);
 
-  const sideContents = !props.isNotFound && !props.isNotCreatable
-    ? (
-      <PageSideContents page={pageWithMeta?.data} />
-    )
-    : <></>;
+  const title = generateCustomTitleForPage(props, pagePath);
 
-  const footerContents = !props.isIdenticalPathPage && !props.isNotFound && pageWithMeta != null
-    ? (
-      <>
-        { pagePath != null && !isTopPagePath && (
-          <Comments pageId={pageId} pagePath={pagePath} revision={pageWithMeta.data.revision} />
-        ) }
-        { isUsersHomePage(pageWithMeta.data.path) && (
-          <UsersHomePageFooter creatorId={pageWithMeta.data.creator._id}/>
-        ) }
-        <PageContentFooter page={pageWithMeta.data} />
-      </>
-    )
-    : <></>;
+  // TODO: show SSR body
+  // const rendererOptions = generateSSRViewOptions(props.rendererConfig, pagePath);
+  // const ssrBody = <RevisionRenderer rendererOptions={rendererOptions} markdown={revisionBody ?? ''} />;
 
   return (
     <>
@@ -350,28 +315,26 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
             <GrowiContextualSubNavigation isLinkSharingDisabled={props.disableLinkSharing} />
           </div>
         </header>
+
         <div className="d-edit-none">
-          <GrowiSubNavigationSwitcher />
+          <GrowiSubNavigationSwitcher isLinkSharingDisabled={props.disableLinkSharing} />
         </div>
 
         <div id="grw-subnav-sticky-trigger" className="sticky-top"></div>
         <div id="grw-fav-sticky-trigger" className="sticky-top"></div>
 
-        <MainPane
-          sideContents={sideContents}
-          footerContents={footerContents}
-        >
-          <PageAlerts />
-          { props.isIdenticalPathPage && <IdenticalPathPage />}
-          { !props.isIdenticalPathPage && (
-            <>
-              { props.isForbidden && <ForbiddenPage /> }
-              { props.isNotCreatable && <NotCreatablePage />}
-              { !props.isForbidden && !props.isNotCreatable && <DisplaySwitcher />}
-            </>
-          ) }
-          <PageStatusAlert />
-        </MainPane>
+        <DisplaySwitcher
+          pageView={
+            <PageView
+              pagePath={pagePath}
+              page={pageWithMeta?.data}
+              // TODO: show SSR body
+              // ssrBody={ssrBody}
+            />
+          }
+        />
+
+        <PageStatusAlert />
 
         {shouldRenderPutbackPageModal && <PutbackPageModal />}
       </div>

+ 0 - 6
packages/app/src/pages/_document.page.tsx

@@ -100,12 +100,6 @@ class GrowiDocument extends Document<GrowiDocumentInitialProps> {
       <Html>
         <Head>
           {this.renderCustomScript(customScript)}
-          <link rel='preload' href="/static/fonts/PressStart2P-latin.woff2" as="font" type="font/woff2" />
-          <link rel='preload' href="/static/fonts/PressStart2P-latin-ext.woff2" as="font" type="font/woff2" />
-          <link rel='preload' href="/static/fonts/Lato-Regular-latin.woff2" as="font" type="font/woff2" />
-          <link rel='preload' href="/static/fonts/Lato-Regular-latin-ext.woff2" as="font" type="font/woff2" />
-          <link rel='preload' href="/static/fonts/Lato-Bold-latin.woff2" as="font" type="font/woff2" />
-          <link rel='preload' href="/static/fonts/Lato-Bold-latin-ext.woff2" as="font" type="font/woff2" />
           <link rel="stylesheet" key="link-theme" href={themeHref} />
           <HeadersForGrowiPlugin pluginResourceEntries={pluginResourceEntries} />
           {this.renderCustomCss(customCss)}

+ 26 - 4
packages/app/src/pages/share/[[...path]].page.tsx

@@ -13,14 +13,17 @@ 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 { Page } from '~/components/Page';
+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 { 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 { PageDocument } from '~/server/models/page';
+import { generateSSRViewOptions } from '~/services/renderer/renderer';
 import {
   useCurrentUser, useCurrentPageId, useRendererConfig, useIsSearchPage, useCurrentPathname,
   useShareLinkId, useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsSearchScopeChildrenAsDefault, useDrawioUri, useIsContainerFluid,
@@ -36,7 +39,6 @@ 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 ShareLinkAlert = dynamic(() => import('~/components/Page/ShareLinkAlert'), { ssr: false });
 const ForbiddenPage = dynamic(() => import('~/components/ForbiddenPage'), { ssr: false });
 
 type Props = CommonProps & {
@@ -105,8 +107,14 @@ const SharedPage: NextPageWithLayout<Props> = (props: Props) => {
   const isShowSharedPage = !props.disableLinkSharing && !isNotFound && !props.isExpired;
   const shareLink = props.shareLink;
 
-  const title = generateCustomTitleForPage(props, props.shareLinkRelatedPage?.path ?? '');
+  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} />
@@ -120,6 +128,18 @@ const SharedPage: NextPageWithLayout<Props> = (props: Props) => {
   //   )
   //   : <></>;
 
+  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>
@@ -166,7 +186,9 @@ const SharedPage: NextPageWithLayout<Props> = (props: Props) => {
           {(isShowSharedPage && shareLink != null) && (
             <>
               <ShareLinkAlert expiredAt={shareLink.expiredAt} createdAt={shareLink.createdAt} />
-              <Page currentPage={props.shareLinkRelatedPage} />
+              <div className="mb-5">
+                { contents }
+              </div>
             </>
           )}
         </MainPane>

+ 20 - 8
packages/app/src/server/middlewares/safe-redirect.js → packages/app/src/server/middlewares/safe-redirect.ts

@@ -4,31 +4,43 @@
  * Usage: app.use(require('middlewares/safe-redirect')(['example.com', 'some.example.com:8080']))
  */
 
+import {
+  Request, Response, NextFunction,
+} from 'express';
+
 import loggerFactory from '~/utils/logger';
 
 const logger = loggerFactory('growi:middleware:safe-redirect');
 
 /**
  * Check whether the redirect url host is in specified whitelist
- * @param {Array<string>} whitelistOfHosts
- * @param {string} redirectToFqdn
  */
-function isInWhitelist(whitelistOfHosts, redirectToFqdn) {
+function isInWhitelist(whitelistOfHosts: string[], redirectToFqdn: string): boolean {
   if (whitelistOfHosts == null || whitelistOfHosts.length === 0) {
     return false;
   }
 
-  const redirectUrl = new URL(redirectToFqdn);
-  return whitelistOfHosts.includes(redirectUrl.hostname) || whitelistOfHosts.includes(redirectUrl.host);
+  try {
+    const redirectUrl = new URL(redirectToFqdn);
+    return whitelistOfHosts.includes(redirectUrl.hostname) || whitelistOfHosts.includes(redirectUrl.host);
+  }
+  catch (err) {
+    logger.warn(err);
+    return false;
+  }
 }
 
 
-module.exports = (whitelistOfHosts) => {
+type ResWithSafeRedirect = Response & {
+  safeRedirect: (redirectTo?: string) => void,
+}
+
+module.exports = (whitelistOfHosts: string[]) => {
 
-  return function(req, res, next) {
+  return function(req: Request, res: ResWithSafeRedirect, next: NextFunction) {
 
     // extend res object
-    res.safeRedirect = function(redirectTo) {
+    res.safeRedirect = function(redirectTo?: string) {
       if (redirectTo == null) {
         return res.redirect('/');
       }

+ 1 - 1
packages/app/src/server/routes/apiv3/page.js

@@ -681,7 +681,7 @@ module.exports = (crowi) => {
     try {
       const fromPage = await Page.findByPath(fromPath, true);
       if (fromPage == null) {
-        return res.apiv3Err(new ErrorV3('fromPage is Null'), 400);
+        return res.apiv3Err(new ErrorV3('fromPage is not exist', 'from-page-is-not-exist'), 400);
       }
 
       const fromPageDescendants = await Page.findManageableListWithDescendants(fromPage, req.user, {}, true);

+ 1 - 0
packages/app/src/services/renderer/remark-plugins/pukiwiki-like-linker.ts

@@ -1,3 +1,4 @@
+// 'mdast-util-wiki-link' and 'micromark-extension-wiki-link' are included in 'remark-wiki-link' package
 import { fromMarkdown, toMarkdown } from 'mdast-util-wiki-link';
 import { syntax } from 'micromark-extension-wiki-link';
 import { Plugin } from 'unified';

+ 53 - 0
packages/app/src/services/renderer/renderer.tsx

@@ -220,6 +220,7 @@ export const generateTocOptions = (config: RendererConfig, tocNode: HtmlElementN
   if (config.isEnabledXssPrevention) {
     verifySanitizePlugin(options);
   }
+
   return options;
 };
 
@@ -283,6 +284,58 @@ export const generateSimpleViewOptions = (
   return options;
 };
 
+export const generateSSRViewOptions = (
+    config: RendererConfig,
+    pagePath: string,
+): RendererOptions => {
+  const options = generateCommonOptions(pagePath);
+
+  const { remarkPlugins, rehypePlugins, components } = options;
+
+  // add remark plugins
+  remarkPlugins.push(
+    math,
+    xsvToTable.remarkPlugin,
+    lsxGrowiPlugin.remarkPlugin,
+    table.remarkPlugin,
+  );
+
+  const isEnabledLinebreaks = config.isEnabledLinebreaks;
+
+  if (isEnabledLinebreaks) {
+    remarkPlugins.push(breaks);
+  }
+
+  if (config.xssOption === RehypeSanitizeOption.CUSTOM) {
+    injectCustomSanitizeOption(config);
+  }
+
+  const rehypeSanitizePlugin: Pluggable<any[]> | (() => void) = config.isEnabledXssPrevention
+    ? [sanitize, deepmerge(
+      commonSanitizeOption,
+      lsxGrowiPlugin.sanitizeOption,
+    )]
+    : () => {};
+
+  // add rehype plugins
+  rehypePlugins.push(
+    [lsxGrowiPlugin.rehypePlugin, { pagePath }],
+    rehypeSanitizePlugin,
+    katex,
+  );
+
+  // add components
+  if (components != null) {
+    components.lsx = LsxImmutable;
+    components.table = Table;
+  }
+
+  if (config.isEnabledXssPrevention) {
+    verifySanitizePlugin(options, false);
+  }
+  return options;
+};
+
 export const generatePreviewOptions = (config: RendererConfig, pagePath: string): RendererOptions => {
   const options = generateCommonOptions(pagePath);
 

+ 6 - 1
packages/app/src/stores/editor.tsx

@@ -10,6 +10,7 @@ import { IEditorSettings } from '~/interfaces/editor-settings';
 import { SlackChannels } from '~/interfaces/user-trigger-notification';
 
 import {
+  useCurrentPathname,
   useCurrentUser, useDefaultIndentSize, useIsGuestUser,
 } from './context';
 // import { localStorageMiddleware } from './middlewares/sync-to-storage';
@@ -18,7 +19,11 @@ import { useStaticSWR } from './use-static-swr';
 
 
 export const useEditingMarkdown = (initialData?: string): SWRResponse<string, Error> => {
-  return useStaticSWR('editingMarkdown', initialData);
+  // need to include useCurrentPathname not useCurrentPagePath
+  // https://github.com/weseek/growi/pull/7301
+  const { data: currentPagePath } = useCurrentPathname();
+
+  return useStaticSWR(['editingMarkdown', currentPagePath], initialData);
 };
 
 

+ 4 - 1
packages/app/src/stores/renderer.tsx

@@ -45,7 +45,7 @@ export const useTocOptions = (): SWRResponse<RendererOptions, Error> => {
   const { data: rendererConfig } = useRendererConfig();
   const { data: tocNode } = useCurrentPageTocNode();
 
-  const isAllDataValid = rendererConfig != null;
+  const isAllDataValid = currentPagePath != null && rendererConfig != null && tocNode != null;
 
   const key = isAllDataValid
     ? ['tocOptions', currentPagePath, tocNode, rendererConfig]
@@ -54,6 +54,9 @@ export const useTocOptions = (): SWRResponse<RendererOptions, Error> => {
   return useSWRImmutable<RendererOptions, Error>(
     key,
     (rendererId, path, tocNode, rendererConfig) => generateTocOptions(rendererConfig, tocNode),
+    {
+      fallbackData: isAllDataValid ? generateTocOptions(rendererConfig, tocNode) : undefined,
+    },
   );
 };
 

+ 3 - 4
packages/app/src/stores/use-static-swr.tsx

@@ -3,7 +3,7 @@ import { useEffect } from 'react';
 import assert from 'assert';
 
 import {
-  Key, SWRConfiguration, SWRResponse,
+  mutate, Key, SWRConfiguration, SWRResponse,
 } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 
@@ -27,10 +27,9 @@ export function useStaticSWR<Data, Error>(
   // Do mutate with `data` from args
   useEffect(() => {
     if (data !== undefined) {
-      swrResponse.mutate(data);
+      mutate(key, data);
     }
-  // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [data]); // Only depends on `data`
+  }, [data, key]);
 
   return swrResponse;
 }

+ 28 - 50
packages/app/src/styles/_mixins.scss

@@ -24,63 +24,41 @@
     + 25px //   add .btn-open-dropzone height
     + 30px; //  add .navbar-editor height
 
-  .main {
+  .editor-root {
     width: 100%;
-    height: calc(100vh - #{$editor-margin-top});
+    height: calc(100vh - #{$header-plus-footer});
+    min-height: calc(100vh - #{$header-plus-footer}); // for IE11
     margin-top: 0px !important;
 
-    .grw-container-convertible {
-      max-width: unset;
-      padding: 0;
-      margin: 0;
-    }
+    // left(editor)
+    .page-editor-editor-container {
+      height: calc(100vh - #{$header-plus-footer});
+      min-height: calc(100vh - #{$header-plus-footer}); // for IE11
 
-    &,
-    .content-main,
-    .tab-content {
-      display: flex;
-      flex: 1;
-      flex-direction: column;
-
-      .tab-pane {
-        height: calc(100vh - #{$header-plus-footer});
-        min-height: calc(100vh - #{$header-plus-footer}); // for IE11
+      .react-codemirror2,
+      .CodeMirror,
+      .CodeMirror-scroll,
+      .textarea-editor {
+        height: calc(100vh - #{$editor-margin});
       }
+    }
 
-      #page-editor {
-        // right(preview)
-        &,
-        & > .row,
-        .page-editor-preview-container,
-        .page-editor-preview-body {
-          height: calc(100vh - #{$header-plus-footer});
-          min-height: calc(100vh - #{$header-plus-footer}); // for IE11
-        }
-
-        // left(editor)
-        .page-editor-editor-container {
-          height: calc(100vh - #{$header-plus-footer});
-          min-height: calc(100vh - #{$header-plus-footer}); // for IE11
-
-          .react-codemirror2,
-          .CodeMirror,
-          .CodeMirror-scroll,
-          .textarea-editor {
-            height: calc(100vh - #{$editor-margin});
-          }
-        }
-      }
+    // right(preview)
+    .page-editor-preview-container,
+    .page-editor-preview-body {
+      height: calc(100vh - #{$header-plus-footer});
+      min-height: calc(100vh - #{$header-plus-footer}); // for IE11
+    }
+  }
 
-      #page-editor-with-hackmd {
-        &,
-        .hackmd-preinit,
-        .hackmd-error,
-        #iframe-hackmd-container > iframe {
-          width: 100%;
-          height: calc(100vh - #{$header-plus-footer});
-          min-height: calc(100vh - #{$header-plus-footer}); // for IE11
-        }
-      }
+  .editor-root#page-editor-with-hackmd {
+    &,
+    .hackmd-preinit,
+    .hackmd-error,
+    #iframe-hackmd-container > iframe {
+      width: 100%;
+      height: calc(100vh - #{$header-plus-footer});
+      min-height: calc(100vh - #{$header-plus-footer}); // for IE11
     }
   }
 }

+ 12 - 4
packages/app/test/cypress/integration/20-basic-features/20-basic-features--click-page-icons.spec.ts

@@ -108,8 +108,12 @@ context('Click page icons button', () => {
     cy.get('#grw-subnav-container').within(() => { cy.screenshot(`${ssPrefix}7-bookmark-page`) });
 
     // total bookmarker
-    cy.get('#po-total-bookmarks').click({force: true});
-    cy.get('.user-list-popover').should('be.visible');
+    cy.waitUntil(() => {
+      // do
+      cy.get('#po-total-bookmarks').click({force: true});
+      // wait until
+      return cy.get('.user-list-popover').then($elem => $elem.is(':visible'));
+    });
     cy.waitUntilSpinnerDisappear();
     cy.get('#grw-subnav-container').within(() => { cy.screenshot(`${ssPrefix}8-bookmarks-counter`) });
 
@@ -129,8 +133,12 @@ context('Click page icons button', () => {
     cy.get('#grw-subnav-container').within(() => { cy.screenshot(`${ssPrefix}9-unbookmark-page`) });
 
     // total bookmarker
-    cy.get('#po-total-bookmarks').click({force: true});
-    cy.get('.user-list-popover').should('be.visible');
+    cy.waitUntil(() => {
+      // do
+      cy.get('#po-total-bookmarks').click({force: true});
+      // wait until
+      return cy.get('.user-list-popover').then($elem => $elem.is(':visible'));
+    });
     cy.waitUntilSpinnerDisappear();
     cy.get('#grw-subnav-container').within(() => { cy.screenshot(`${ssPrefix}10-bookmarks-counter`) });
   });

+ 66 - 0
packages/app/test/cypress/integration/22-sharelink/22-sharelink--access-to-sharelink.spec.ts

@@ -0,0 +1,66 @@
+context('Access to sharelink by guest', () => {
+  const ssPrefix = 'access-to-sharelink-by-guest-';
+
+  let createdSharelinkId: string;
+
+  it('Prepare sharelink', () => {
+    // login
+    cy.fixture("user-admin.json").then(user => {
+      cy.login(user.username, user.password);
+    });
+
+    cy.visit('/Sandbox/Bootstrap4');
+
+    // open dropdown
+    cy.waitUntil(() => {
+      // do
+      cy.getByTestid('grw-contextual-sub-nav').should('be.visible').within(() => {
+        cy.getByTestid('open-page-item-control-btn').find('button').first().as('btn').click();
+      });
+      // wait until
+      return cy.get('body').within(() => {
+        return Cypress.$('.dropdown-menu.show').is(':visible');
+      });
+    });
+
+    // open modal
+    cy.get('.dropdown-menu.show').should('be.visible').within(() => {
+      cy.getByTestid('open-page-accessories-modal-btn-with-share-link-management-data-tab').click({force: true});
+    });
+    cy.waitUntilSpinnerDisappear();
+    cy.getByTestid('page-accessories-modal').should('be.visible');
+    cy.getByTestid('share-link-management').should('be.visible');
+
+    // create share link
+    cy.getByTestid('share-link-management').within(() => {
+      cy.getByTestid('btn-sharelink-toggleform').should('be.visible').click();
+      cy.getByTestid('btn-sharelink-issue').should('be.visible').click();
+
+      cy.get('tbody')
+        .find('tr').first() // the first row
+        .find('td').first() // the first column
+        .find('span').first().then((elem) => {
+
+        // store id
+        createdSharelinkId = elem.text();
+        // overwrite the label
+        elem.html('63d100000000000000000000');
+      });
+    });
+
+    cy.getByTestid('page-accessories-modal').within(() => { cy.screenshot(`${ssPrefix}-sharelink-created`) });
+  });
+
+  it('The sharelink page is successfully loaded', () => {
+    cy.visit(`/share/${createdSharelinkId}`);
+
+    cy.getByTestid('grw-contextual-sub-nav').should('be.visible');
+    cy.get('.wiki').should('be.visible');
+
+    cy.waitUntilSkeletonDisappear();
+    cy.screenshot(`${ssPrefix}-access-to-sharelink`);
+  });
+
+});
+
+

+ 1 - 1
packages/app/tsconfig.build.server.json

@@ -11,7 +11,7 @@
     "paths": {
       "~/*": ["./src/*"],
       "^/*": ["./*"],
-      "@growi/remark-lsx/*": ["../remark-lsx/dist/cjs/*"],
+      "@growi/remark-lsx/*": ["../remark-lsx/dist/*"],
       "debug": ["./src/utils/logger/alias-for-debug"]
     }
   },

+ 1 - 1
packages/codemirror-textlint/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/codemirror-textlint",
-  "version": "6.0.3-RC.0",
+  "version": "6.0.5-RC.0",
   "license": "MIT",
   "main": "dist/index.js",
   "scripts": {

+ 1 - 1
packages/core/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/core",
-  "version": "6.0.3-RC.0",
+  "version": "6.0.5-RC.0",
   "description": "GROWI Core Libraries",
   "license": "MIT",
   "keywords": [

+ 4 - 0
packages/core/src/test/util/page-path-utils.test.js

@@ -104,6 +104,10 @@ describe('isCreatablePage test', () => {
     expect(isCreatablePage('http://demo.growi.org/hoge')).toBeFalsy();
     expect(isCreatablePage('https://demo.growi.org/hoge')).toBeFalsy();
 
+    // include backslash
+    expect(isCreatablePage('/foo\\/bar')).toBeFalsy();
+    expect(isCreatablePage('/foo\\\\bar')).toBeFalsy();
+
     expect(isCreatablePage('/_search')).toBeFalsy();
     expect(isCreatablePage('/_search/foo')).toBeFalsy();
     expect(isCreatablePage('/_private-legacy-pages')).toBeFalsy();

+ 1 - 0
packages/core/src/utils/page-path-utils.ts

@@ -117,6 +117,7 @@ const restrictedPatternsToCreate: Array<RegExp> = [
   /.+\.md$/,
   /^(\.\.)$/, // see: https://github.com/weseek/growi/issues/3582
   /(\/\.\.)\/?/, // see: https://github.com/weseek/growi/issues/3582
+  /\\/, // see: https://github.com/weseek/growi/issues/7241
   /^\/(_search|_private-legacy-pages)(\/.*|$)/,
   /^\/(installer|register|login|logout|admin|me|files|trash|paste|comments|tags|share)(\/.*|$)/,
   /^\/user\/[^/]+$/, // see: https://regex101.com/r/utVQct/1

+ 1 - 1
packages/hackmd/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/hackmd",
-  "version": "6.0.3-RC.0",
+  "version": "6.0.5-RC.0",
   "description": "GROWI js and css files to use hackmd",
   "license": "MIT",
   "main": "dist/index.js",

+ 1 - 1
packages/preset-themes/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@growi/preset-themes",
   "description": "GROWI preset themes",
-  "version": "6.0.3-RC.0",
+  "version": "6.0.5-RC.0",
   "license": "MIT",
   "main": "dist/libs/index.js",
   "files": [

+ 1 - 1
packages/preset-themes/src/styles/antarctic.scss

@@ -41,7 +41,7 @@
   --bgcolor-global-l: 97%;
   --bgcolor-inline-code: #{$gray-100}; //optional
   --bgcolor-card: #{$gray-50};
-  --bgcolor-blinked-section: #{hsl.alpha(var(--primary), 85%)};
+  --bgcolor-blinked-section: #{hsl.alpha(var(--primary), 15%)};
   //--bgcolor-keyword-highlighted: #{$grw-marker-yellow};
 
   // Font colors

+ 2 - 2
packages/preset-themes/src/styles/default.scss

@@ -22,7 +22,7 @@
   --bgcolor-global-l: 100%;
   --bgcolor-inline-code: #{$gray-100}; //optional
   --bgcolor-card: #{$gray-50};
-  --bgcolor-blinked-section: #{hsl.alpha(var(--primary),90%)};
+  --bgcolor-blinked-section: #{hsl.alpha(var(--primary),10%)};
   // --bgcolor-keyword-highlighted: #{$grw-marker-yellow};
 
   // Font colors
@@ -154,7 +154,7 @@
   --bgcolor-global-l: 8%;
   --bgcolor-inline-code: #1f1f22; //optional
   --bgcolor-card: #{hsl.darken(var(--bgcolor-global),5%)};
-  --bgcolor-blinked-section: #{hsl.alpha(var(--primary), 60%)};
+  --bgcolor-blinked-section: #{hsl.alpha(var(--primary), 40%)};
   --bgcolor-keyword-highlighted: #{darken($grw-marker-red, 30%)};
 
   // Font colors

+ 1 - 1
packages/preset-themes/src/styles/fire-red.scss

@@ -21,7 +21,7 @@
   --bgcolor-global-l: 100%;
   --bgcolor-inline-code: #{$gray-100}; //optional
   --bgcolor-card: var(--accentcolor);
-  --bgcolor-blinked-section: #{hsl.alpha(var(--primary),90%)};
+  --bgcolor-blinked-section: #{hsl.alpha(var(--primary),10%)};
   //--bgcolor-keyword-highlighted: #{$grw-marker-yellow};
 
   // Font colors

+ 1 - 1
packages/preset-themes/src/styles/future.scss

@@ -23,7 +23,7 @@
   --bgcolor-global-l: var(--themecolor-l);
   --bgcolor-inline-code: #1f1f22; //optional
   --bgcolor-card: #{hsl.darken(var(--themecolor), 5%)};
-  --bgcolor-blinked-section: #{hsl.alpha(var(--primary), 60%)};
+  --bgcolor-blinked-section: #{hsl.alpha(var(--primary), 40%)};
   --bgcolor-keyword-highlighted: #{darken($grw-marker-red, 30%)};
 
   // Font colors

+ 1 - 1
packages/preset-themes/src/styles/halloween.scss

@@ -30,7 +30,7 @@ $bordercolor: #7e0d7e;
   --bgcolor-global-l: 1%;
   --bgcolor-inline-code: #1f1f22; //optional
   --bgcolor-card: var(--bgcolor-global);
-  --bgcolor-blinked-section: #{hsl.alpha(var(--primary), 60%)};
+  --bgcolor-blinked-section: #{hsl.alpha(var(--primary), 40%)};
   --bgcolor-keyword-highlighted: darkviolet;
 
   // Font colors

+ 1 - 1
packages/preset-themes/src/styles/island.scss

@@ -20,7 +20,7 @@
   --bgcolor-global-l: 90%;
   --bgcolor-card: #{$gray-50};
   --bgcolor-inline-code: #{$gray-100}; //optional
-  --bgcolor-blinked-section: #{hsl.alpha(var(--primary), 70%)};
+  --bgcolor-blinked-section: #{hsl.alpha(var(--primary), 30%)};
   //--bgcolor-keyword-highlighted: #{$grw-marker-yellow};
 
   // Font colors

+ 1 - 1
packages/preset-themes/src/styles/jade-green.scss

@@ -20,7 +20,7 @@
   --bgcolor-global-l: 100%;
   --bgcolor-inline-code: #{$gray-100}; //optional
   --bgcolor-card: var(--accentcolor);
-  --bgcolor-blinked-section: #{hsl.alpha(var(--primary),90%)};
+  --bgcolor-blinked-section: #{hsl.alpha(var(--primary),10%)};
   //--bgcolor-keyword-highlighted: #{$grw-marker-yellow};
 
   // Font colors

+ 1 - 1
packages/preset-themes/src/styles/kibela.scss

@@ -23,7 +23,7 @@
   --bgcolor-global-l: 96%;
   --bgcolor-inline-code: #{hsl.lighten(var(--subthemecolor), 70%)};
   --bgcolor-card: var(--lightthemecolor);
-  --bgcolor-blinked-section: #{hsl.alpha(var(--primary),80%)};
+  --bgcolor-blinked-section: #{hsl.alpha(var(--primary),20%)};
   //--bgcolor-keyword-highlighted: #{$grw-marker-yellow};
 
   // Font colors

+ 1 - 1
packages/preset-themes/src/styles/mono-blue.scss

@@ -21,7 +21,7 @@
   --bgcolor-global-l: 98%;
   --bgcolor-inline-code: #{$gray-100}; //optional
   --bgcolor-card: #{hsl.darken(var(--bgcolor-global), 5%)};
-  --bgcolor-blinked-section: #{hsl.alpha(var(--primary),90%)};
+  --bgcolor-blinked-section: #{hsl.alpha(var(--primary),10%)};
   //--bgcolor-keyword-highlighted: #{$grw-marker-yellow};
 
   // Font colors

+ 1 - 1
packages/preset-themes/src/styles/nature.scss

@@ -28,7 +28,7 @@
   --bgcolor-global-l: 99%;
   --bgcolor-inline-code: #{$gray-100}; //optional
   --bgcolor-card: #f1ffe4;
-  --bgcolor-blinked-section: #{hsl.alpha(var(--primary), 90%)};
+  --bgcolor-blinked-section: #{hsl.alpha(var(--primary), 10%)};
   //--bgcolor-keyword-highlighted: #{$grw-marker-yellow};
 
   // Font colors

+ 1 - 1
packages/preset-themes/src/styles/wood.scss

@@ -40,7 +40,7 @@
   --bgcolor-global-hs: 0,0%;
   --bgcolor-global-l: 100%;
   --bgcolor-card: #ece8de;
-  --bgcolor-blinked-section: #{hsl.alpha(var(--primary),70%)};
+  --bgcolor-blinked-section: #{hsl.alpha(var(--primary),30%)};
   --bgcolor-keyword-highlighted: #{$grw-marker-blue};
 
   // Font colors

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

@@ -1,6 +1,6 @@
 {
   "name": "@growi/remark-drawio",
-  "version": "6.0.3-RC.0",
+  "version": "6.0.5-RC.0",
   "description": "remark plugin to draw diagrams with draw.io (diagrams.net)",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/remark-growi-directive/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/remark-growi-directive",
-  "version": "6.0.3-RC.0",
+  "version": "6.0.5-RC.0",
   "description": "remark plugin to support GROWI plugin (forked from remark-directive@2.0.1)",
   "license": "MIT",
   "keywords": [

+ 9 - 12
packages/remark-lsx/package.json

@@ -1,21 +1,18 @@
 {
   "name": "@growi/remark-lsx",
-  "version": "6.0.3-RC.0",
+  "version": "6.0.5-RC.0",
   "description": "GROWI plugin to list pages",
   "license": "MIT",
   "keywords": ["growi", "growi-plugin"],
-  "main": "dist/cjs/index.js",
-  "module": "dist/esm/index.js",
+  "main": "dist/index.js",
   "exports": {
-    "./components": "./dist/cjs/components/index.js",
-    "./services/renderer": "./dist/cjs/services/renderer/index.js",
-    "./server/routes": "./dist/cjs/server/routes/index.js"
+    "./components": "./dist/components/index.js",
+    "./services/renderer": "./dist/services/renderer/index.js",
+    "./server/routes": "./dist/server/routes/index.js"
   },
   "files": ["dist"],
   "scripts": {
-    "build": "run-p build:*",
-    "build:cjs": "tsc -p tsconfig.build.cjs.json && tsc-alias -p tsconfig.build.cjs.json",
-    "build:esm": "tsc -p tsconfig.build.esm.json && tsc-alias -p tsconfig.build.esm.json",
+    "build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
     "clean": "npx -y shx rm -rf dist",
     "lint:js": "eslint **/*.{js,jsx,ts,tsx}",
     "lint:styles": "stylelint --allow-empty-input src/**/*.scss src/**/*.css",
@@ -23,9 +20,9 @@
     "test": ""
   },
   "dependencies": {
-    "@growi/core": "^6.0.3-RC.0",
-    "@growi/remark-growi-directive": "^6.0.3-RC.0",
-    "@growi/ui": "^6.0.3-RC.0",
+    "@growi/core": "^6.0.5-RC.0",
+    "@growi/remark-growi-directive": "^6.0.5-RC.0",
+    "@growi/ui": "^6.0.5-RC.0",
     "swr": "^1.3.0"
   },
   "devDependencies": {

+ 10 - 3
packages/remark-lsx/src/services/renderer/lsx.ts

@@ -4,13 +4,14 @@ import { pathUtils } from '@growi/core';
 import { remarkGrowiDirectivePluginType } from '@growi/remark-growi-directive';
 import { Schema as SanitizeOption } from 'hast-util-sanitize';
 import { selectAll, HastNode } from 'hast-util-select';
+import isAbsolute from 'is-absolute-url';
 import { Plugin } from 'unified';
 import { visit } from 'unist-util-visit';
 
 const NODE_NAME_PATTERN = new RegExp(/ls|lsx/);
 const SUPPORTED_ATTRIBUTES = ['prefix', 'num', 'depth', 'sort', 'reverse', 'filter', 'except'];
 
-const { hasHeadingSlash } = pathUtils;
+const { addHeadingSlash, hasHeadingSlash } = pathUtils;
 
 type DirectiveAttributes = Record<string, string>
 
@@ -66,10 +67,16 @@ export type LsxRehypePluginParams = {
   pagePath?: string,
 }
 
-const pathResolver = (relativeHref: string, basePath: string): string => {
+const pathResolver = (href: string, basePath: string): string => {
+  // exclude absolute URL
+  if (isAbsolute(href)) {
+    // remove scheme
+    return href.replace(/^(.+?):\/\//, '/');
+  }
+
   // generate relative pathname
   const baseUrl = new URL(pathUtils.addTrailingSlash(basePath), 'https://example.com');
-  const relativeUrl = new URL(relativeHref, baseUrl);
+  const relativeUrl = new URL(href, baseUrl);
 
   return relativeUrl.pathname;
 };

+ 0 - 18
packages/remark-lsx/tsconfig.build.esm.json

@@ -1,18 +0,0 @@
-{
-  "extends": "./tsconfig.base.json",
-  "compilerOptions": {
-    "module": "esnext",
-
-    "rootDir": "./src",
-    "outDir": "dist/esm",
-    "declaration": true,
-    "noResolve": false,
-    "preserveConstEnums": true,
-    "sourceMap": false,
-    "noEmit": false,
-
-    "baseUrl": ".",
-    "paths": {
-    }
-  }
-}

+ 1 - 1
packages/remark-lsx/tsconfig.build.cjs.json → packages/remark-lsx/tsconfig.build.json

@@ -2,7 +2,7 @@
   "extends": "./tsconfig.base.json",
   "compilerOptions": {
     "rootDir": "./src",
-    "outDir": "dist/cjs",
+    "outDir": "dist",
     "declaration": true,
     "noResolve": false,
     "preserveConstEnums": true,

+ 1 - 1
packages/slack/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/slack",
-  "version": "6.0.3-RC.0",
+  "version": "6.0.5-RC.0",
   "license": "MIT",
   "main": "dist/index.js",
   "typings": "dist/index.d.ts",

+ 2 - 2
packages/slackbot-proxy/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/slackbot-proxy",
-  "version": "6.0.3-slackbot-proxy.0",
+  "version": "6.0.5-slackbot-proxy.0",
   "license": "MIT",
   "scripts": {
     "build": "yarn tsc && tsc-alias -p tsconfig.build.json",
@@ -26,7 +26,7 @@
   },
   "dependencies": {
     "@godaddy/terminus": "^4.9.0",
-    "@growi/slack": "^6.0.3-RC.0",
+    "@growi/slack": "^6.0.5-RC.0",
     "@slack/oauth": "^2.0.1",
     "@slack/web-api": "^6.2.4",
     "@tsed/common": "^6.43.0",

+ 2 - 2
packages/ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/ui",
-  "version": "6.0.3-RC.0",
+  "version": "6.0.5-RC.0",
   "description": "GROWI UI Libraries",
   "license": "MIT",
   "keywords": ["growi"],
@@ -17,7 +17,7 @@
     "test": "jest --verbose"
   },
   "dependencies": {
-    "@growi/core": "^6.0.3-RC.0"
+    "@growi/core": "^6.0.5-RC.0"
   },
   "devDependencies": {
     "eslint-plugin-regex": "^1.8.0",

+ 3 - 3
yarn.lock

@@ -22878,9 +22878,9 @@ typpy@2.3.11:
     function.name "^1.0.3"
 
 ua-parser-js@^0.7.18, ua-parser-js@^0.7.9:
-  version "0.7.31"
-  resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.31.tgz#649a656b191dffab4f21d5e053e27ca17cbff5c6"
-  integrity sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==
+  version "0.7.33"
+  resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.33.tgz#1d04acb4ccef9293df6f70f2c3d22f3030d8b532"
+  integrity sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw==
 
 uberproto@^1.1.0:
   version "1.2.0"