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

Merge branch 'support/apply-nextjs-2' into imprv/1000059-show-customize-page

kaori 3 лет назад
Родитель
Сommit
893d35c673
100 измененных файлов с 1396 добавлено и 1350 удалено
  1. 1 1
      lerna.json
  2. 2 2
      package.json
  3. 0 1
      packages/app/.env.development
  4. 2 2
      packages/app/bin/github-actions/update-readme.sh
  5. 6 6
      packages/app/docker/README.md
  6. 19 4
      packages/app/next.config.js
  7. 16 19
      packages/app/package.json
  8. 6 0
      packages/app/public/static/locales/en_US/translation.json
  9. 6 0
      packages/app/public/static/locales/ja_JP/translation.json
  10. 6 0
      packages/app/public/static/locales/zh_CN/translation.json
  11. 2 2
      packages/app/src/client/app.jsx
  12. 2 5
      packages/app/src/client/base.jsx
  13. 0 4
      packages/app/src/client/legacy/crowi.js
  14. 4 4
      packages/app/src/client/models/MarkdownTable.js
  15. 1 2
      packages/app/src/client/plugin.js
  16. 1 24
      packages/app/src/client/services/AdminSocketIoContainer.js
  17. 12 35
      packages/app/src/client/services/AppContainer.js
  18. 35 1
      packages/app/src/client/services/ContextExtractor.tsx
  19. 2 2
      packages/app/src/client/services/PageContainer.js
  20. 0 208
      packages/app/src/client/util/GrowiRenderer.js
  21. 0 23
      packages/app/src/client/util/PreProcessor/XssFilter.js
  22. 1 0
      packages/app/src/client/util/blink-section-header.ts
  23. 4 0
      packages/app/src/client/util/i18n.js
  24. 0 17
      packages/app/src/client/util/markdown-it/blockdiag.js
  25. 0 9
      packages/app/src/client/util/markdown-it/drawio-viewer.js
  26. 0 12
      packages/app/src/client/util/markdown-it/emoji.js
  27. 0 16
      packages/app/src/client/util/markdown-it/mathjax.js
  28. 0 18
      packages/app/src/client/util/markdown-it/task-lists.js
  29. 0 27
      packages/app/src/client/util/markdown-it/toc-and-anchor.js
  30. 4 7
      packages/app/src/client/util/reveal/plugins/growi-renderer.js
  31. 23 18
      packages/app/src/components/Admin/App/AppSetting.jsx
  32. 1 2
      packages/app/src/components/Admin/App/AppSettingsPageContents.tsx
  33. 45 51
      packages/app/src/components/Admin/ExportArchiveDataPage.jsx
  34. 4 6
      packages/app/src/components/Admin/MarkdownSetting/MarkDownSettingContents.tsx
  35. 2 4
      packages/app/src/components/Admin/MarkdownSetting/PresentationForm.jsx
  36. 2 4
      packages/app/src/components/Admin/MarkdownSetting/WhiteListInput.jsx
  37. 2 4
      packages/app/src/components/Admin/MarkdownSetting/XssForm.jsx
  38. 2 4
      packages/app/src/components/Admin/Notification/GlobalNotification.jsx
  39. 2 4
      packages/app/src/components/Admin/Notification/GlobalNotificationList.jsx
  40. 9 12
      packages/app/src/components/Admin/Notification/NotificationSetting.jsx
  41. 2 4
      packages/app/src/components/Admin/Notification/UserNotificationRow.jsx
  42. 2 4
      packages/app/src/components/Admin/Notification/UserTriggerNotification.jsx
  43. 5 5
      packages/app/src/components/Admin/Security/FacebookSecuritySetting.jsx
  44. 21 21
      packages/app/src/components/Admin/Security/SecurityManagementContents.jsx
  45. 4 6
      packages/app/src/components/Admin/Security/ShareLinkSetting.jsx
  46. 18 0
      packages/app/src/components/BookmarkButtons.module.scss
  47. 3 1
      packages/app/src/components/BookmarkButtons.tsx
  48. 17 0
      packages/app/src/components/LikeButtons.module.scss
  49. 4 15
      packages/app/src/components/LikeButtons.tsx
  50. 0 215
      packages/app/src/components/MyDraftList/Draft.jsx
  51. 144 0
      packages/app/src/components/MyDraftList/Draft.tsx
  52. 1 1
      packages/app/src/components/MyDraftList/MyDraftList.jsx
  53. 34 47
      packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  54. 6 3
      packages/app/src/components/Navbar/GrowiNavbar.tsx
  55. 199 0
      packages/app/src/components/Navbar/GrowiSubNavigation.module.scss
  56. 9 2
      packages/app/src/components/Navbar/GrowiSubNavigation.tsx
  57. 7 15
      packages/app/src/components/Navbar/PageEditorModeManager.jsx
  58. 34 0
      packages/app/src/components/Navbar/PageEditorModeManager.module.scss
  59. 18 0
      packages/app/src/components/NotCreatablePage.tsx
  60. 141 137
      packages/app/src/components/Page.jsx
  61. 7 5
      packages/app/src/components/Page/DisplaySwitcher.tsx
  62. 0 1
      packages/app/src/components/Page/RevisionBody.jsx
  63. 4 4
      packages/app/src/components/Page/RevisionLoader.jsx
  64. 0 205
      packages/app/src/components/Page/RevisionRenderer.jsx
  65. 176 0
      packages/app/src/components/Page/RevisionRenderer.tsx
  66. 12 10
      packages/app/src/components/PageAlert/PageGrantAlert.tsx
  67. 12 11
      packages/app/src/components/PageAlert/PageStaleAlert.tsx
  68. 9 3
      packages/app/src/components/PageComment.tsx
  69. 7 6
      packages/app/src/components/PageComment/Comment.jsx
  70. 7 7
      packages/app/src/components/PageComment/CommentEditor.tsx
  71. 7 2
      packages/app/src/components/PageComment/CommentEditorLazyRenderer.tsx
  72. 4 2
      packages/app/src/components/PageComment/ReplayComments.jsx
  73. 9 1
      packages/app/src/components/PageEditor.tsx
  74. 8 9
      packages/app/src/components/PageEditor/Preview.tsx
  75. 13 4
      packages/app/src/components/PageTimeline.jsx
  76. 49 0
      packages/app/src/components/ReactMarkdownComponents/Header.tsx
  77. 46 0
      packages/app/src/components/ReactMarkdownComponents/NextLink.tsx
  78. 5 5
      packages/app/src/components/SearchPage/SearchResultContent.tsx
  79. 1 1
      packages/app/src/components/Sidebar.tsx
  80. 8 10
      packages/app/src/components/Sidebar/CustomSidebar.tsx
  81. 7 3
      packages/app/src/components/Sidebar/SidebarNav.tsx
  82. 14 0
      packages/app/src/components/SubscribeButton.module.scss
  83. 3 1
      packages/app/src/components/SubscribeButton.tsx
  84. 10 8
      packages/app/src/components/TableOfContents.jsx
  85. 33 0
      packages/app/src/interfaces/activity.ts
  86. 4 3
      packages/app/src/interfaces/global.ts
  87. 20 0
      packages/app/src/interfaces/services/renderer.ts
  88. 2 1
      packages/app/src/migrations/20180927102719-init-serverurl.js
  89. 2 1
      packages/app/src/migrations/20190618055300-abolish-crowi-classic-auth.js
  90. 2 1
      packages/app/src/migrations/20190618104011-add-config-app-installed.js
  91. 2 1
      packages/app/src/migrations/20200420160390-remove-crowi-layout.js
  92. 2 1
      packages/app/src/migrations/20200512005851-remove-behavior-type.js
  93. 2 1
      packages/app/src/migrations/20200514001356-update-theme-color-for-dark.js
  94. 2 1
      packages/app/src/migrations/20200620203632-normalize-locale-id.js
  95. 2 1
      packages/app/src/migrations/20200827045151-remove-layout-setting.js
  96. 2 1
      packages/app/src/migrations/20200828024025-copy-aws-setting.js
  97. 2 1
      packages/app/src/migrations/20200901034313-update-mail-transmission.js
  98. 3 1
      packages/app/src/migrations/20200903080025-remove-timeline-type.js.js
  99. 3 1
      packages/app/src/migrations/20200915035234-rename-s3-config.js
  100. 2 1
      packages/app/src/migrations/20210830074539-update-configs-for-slackbot.js

+ 1 - 1
lerna.json

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

+ 2 - 2
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "5.0.12-RC.0",
+  "version": "5.1.0-RC.2",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -61,7 +61,7 @@
     "eslint": "^8.18.0",
     "eslint-config-next": "^12.1.6",
     "eslint-config-weseek": "^2.1.0",
-    "eslint-import-resolver-typescript": "^3.1.1",
+    "eslint-import-resolver-typescript": "^3.2.5",
     "eslint-plugin-import": "^2.26.0",
     "eslint-plugin-jest": "^26.5.3",
     "eslint-plugin-react": "^7.30.1",

+ 0 - 1
packages/app/.env.development

@@ -7,7 +7,6 @@ MIGRATIONS_DIR=src/migrations/
 APP_SITE_URL=http://localhost:3000
 FILE_UPLOAD=mongodb
 # MONGO_GRIDFS_TOTAL_LIMIT=10485760
-MATHJAX=1
 # NO_CDN=true
 MONGO_URI="mongodb://mongo:27017/growi"
 # REDIS_URI="http://redis:6379"

+ 2 - 2
packages/app/bin/github-actions/update-readme.sh

@@ -2,5 +2,5 @@
 
 cd docker
 
-sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`5\.0\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/docker\/Dockerfile.\+\)$/\1${RELEASED_VERSION}\2\3${RELEASED_VERSION}\4/" README.md
-sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`5\.0-nocdn\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/docker\/Dockerfile.\+\)$/\1${RELEASED_VERSION}-nocdn\2\3${RELEASED_VERSION}\4/" README.md
+sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`5\.1\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/docker\/Dockerfile.\+\)$/\1${RELEASED_VERSION}\2\3${RELEASED_VERSION}\4/" README.md
+sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`5\.1-nocdn\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/docker\/Dockerfile.\+\)$/\1${RELEASED_VERSION}-nocdn\2\3${RELEASED_VERSION}\4/" README.md

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

@@ -10,12 +10,12 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 
-* [`5.0.11`, `5.0`, `5`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.11/docker/Dockerfile)
-* [`5.0.11-nocdn`, `5.0-nocdn`, `5-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.11/docker/Dockerfile)
-* [`4.5.22`, `4.5`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.22/docker/Dockerfile)
-* [`4.5.22-nocdn`, `4.5-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.22/docker/Dockerfile)
-* [`4.4.13`, `4.4` (Dockerfile)](https://github.com/weseek/growi/blob/v4.4.13/docker/Dockerfile)
-* [`4.4.13-nocdn`, `4.4-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.4.13/docker/Dockerfile)
+* [`5.1.0`, `5.1`, `5`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v5.1.0/packages/app/docker/Dockerfile)
+* [`5.1.0-nocdn`, `5.1-nocdn`, `5-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v5.1.0/packages/app/docker/Dockerfile)
+* [`5.0.11`, `5.0` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.11/packages/app/docker/Dockerfile)
+* [`5.0.11-nocdn`, `5.0-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.11/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)
+* [`4.5.23-nocdn`, `4.5-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.23/packages/app/docker/Dockerfile)
 
 
 What is GROWI?

+ 19 - 4
packages/app/next.config.js

@@ -3,7 +3,7 @@ import { I18NextHMRPlugin } from 'i18next-hmr/plugin';
 import { WebpackManifestPlugin } from 'webpack-manifest-plugin';
 
 import { i18n, localePath } from './src/next-i18next.config';
-import { listScopedPackages } from './src/utils/next.config.utils';
+import { listScopedPackages, listPrefixedPackages } from './src/utils/next.config.utils';
 
 
 // setup logger
@@ -15,12 +15,23 @@ const logger = eazyLogger.Logger({
 
 const setupWithTM = () => {
   // define transpiled packages for '@growi/*'
-  const scopedPackages = listScopedPackages(['@growi'], { ignorePackageNames: '@growi/app' });
+  const packages = [
+    ...listScopedPackages(['@growi'], { ignorePackageNames: '@growi/app' }),
+    // listing ESM packages until experimental.esmExternals works correctly to avoid ERR_REQUIRE_ESM
+    'react-markdown',
+    'unified',
+    'comma-separated-tokens',
+    'decode-named-character-reference',
+    'space-separated-tokens',
+    'trim-lines',
+    'emoticon',
+    ...listPrefixedPackages(['remark-', 'rehype-', 'hast-', 'mdast-', 'micromark-', 'micromark-', 'unist-']),
+  ];
 
   logger.info('{bold:Listing scoped packages for transpiling:}');
-  logger.unprefixed('info', `{grey:${JSON.stringify(scopedPackages, null, 2)}}`);
+  logger.unprefixed('info', `{grey:${JSON.stringify(packages, null, 2)}}`);
 
-  return require('next-transpile-modules')(scopedPackages);
+  return require('next-transpile-modules')(packages);
 };
 const withTM = setupWithTM();
 
@@ -33,6 +44,10 @@ const additionalWebpackEntries = {
 
 /** @type {import('next').NextConfig} */
 const nextConfig = {
+  // == DOES NOT WORK
+  // see: https://github.com/vercel/next.js/discussions/27876
+  // experimental: { esmExternals: true }, // Prefer loading of ES Modules over CommonJS
+
   reactStrictMode: true,
   typescript: {
     tsconfigPath: 'tsconfig.build.client.json',

+ 16 - 19
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "5.0.12-RC.0",
+  "version": "5.1.0-RC.2",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -63,11 +63,11 @@
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^5.0.12-RC.0",
-    "@growi/plugin-attachment-refs": "^5.0.12-RC.0",
-    "@growi/plugin-lsx": "^5.0.12-RC.0",
-    "@growi/plugin-pukiwiki-like-linker": "^5.0.12-RC.0",
-    "@growi/slack": "^5.0.12-RC.0",
+    "@growi/codemirror-textlint": "^5.1.0-RC.2",
+    "@growi/plugin-attachment-refs": "^5.1.0-RC.2",
+    "@growi/plugin-lsx": "^5.1.0-RC.2",
+    "@growi/plugin-pukiwiki-like-linker": "^5.1.0-RC.2",
+    "@growi/slack": "^5.1.0-RC.2",
     "@promster/express": "^7.0.2",
     "@promster/server": "^7.0.4",
     "@slack/events-api": "^3.0.0",
@@ -149,9 +149,16 @@
     "react-dnd-html5-backend": "^14.1.0",
     "react-dom": "^18.2.0",
     "react-image-crop": "^8.3.0",
+    "react-markdown": "^8.0.3",
     "react-multiline-clamp": "^2.0.0",
     "reconnecting-websocket": "^4.4.0",
     "redis": "^3.0.2",
+    "rehype-slug": "^5.0.1",
+    "rehype-toc": "^3.0.2",
+    "remark-breaks": "^3.0.2",
+    "remark-emoji": "^3.0.2",
+    "remark-footnotes": "^4.0.1",
+    "remark-gfm": "^3.0.1",
     "rimraf": "^3.0.0",
     "socket.io": "^4.2.0",
     "stream-to-promise": "^3.0.0",
@@ -171,7 +178,8 @@
     "handsontable": "v7.0.0 or above is no loger MIT lisence."
   },
   "devDependencies": {
-    "@growi/ui": "^5.0.12-RC.0",
+    "@alienfast/i18next-loader": "^1.1.4",
+    "@growi/ui": "^5.1.0-RC.2",
     "@handsontable/react": "=2.1.0",
     "@types/compression": "^1.7.0",
     "@types/express": "^4.17.11",
@@ -199,17 +207,6 @@
     "jquery.cookie": "~1.4.1",
     "jshint": "^2.13.0",
     "load-css-file": "^1.0.0",
-    "markdown-it": "^10.0.0",
-    "markdown-it-blockdiag": "^1.1.1",
-    "markdown-it-drawio-viewer": "^1.3.1",
-    "markdown-it-emoji": "^1.4.0",
-    "markdown-it-emoji-mart": "^0.1.1",
-    "markdown-it-footnote": "^3.0.1",
-    "markdown-it-mathjax": "^2.0.0",
-    "markdown-it-named-headers": "^0.0.4",
-    "markdown-it-plantuml": "^1.3.0",
-    "markdown-it-task-checkbox": "^1.0.6",
-    "markdown-it-toc-and-anchor-with-slugid": "^1.1.4",
     "markdown-table": "^1.1.1",
     "material-icons": "^1.11.3",
     "morgan": "^1.10.0",
@@ -235,7 +232,7 @@
     "socket.io-client": "^4.2.0",
     "sticky-events": "^3.4.11",
     "swagger2openapi": "^5.3.1",
-    "swr": "^1.1.2",
+    "swr": "^1.3.0",
     "throttle-debounce": "^3.0.1",
     "toastr": "^2.1.2",
     "ts-node-dev": "^2.0.0",

+ 6 - 0
packages/app/public/static/locales/en_US/translation.json

@@ -1,4 +1,7 @@
 {
+  "meta": {
+    "display_name": "English"
+  },
   "Help": "Help",
   "view": "View",
   "Edit": "Edit",
@@ -195,6 +198,9 @@
     "page_not_exist": "This page does not exist.",
     "page_not_exist_alert": "This page does not exist. Please create a new page."
   },
+  "not_creatable_page": {
+    "could_not_creata_path": "Couldn't create path."
+  },
   "custom_navigation": {
     "no_page_list": "There are no pages under this page.",
     "link_sharing_is_disabled": "Link sharing is disabled."

+ 6 - 0
packages/app/public/static/locales/ja_JP/translation.json

@@ -1,4 +1,7 @@
 {
+  "meta": {
+    "display_name": "日本語"
+  },
   "Help": "ヘルプ",
   "view": "View",
   "Edit": "編集",
@@ -197,6 +200,9 @@
     "page_not_exist": "このページは存在しません。",
     "page_not_exist_alert": "このページは存在しません。新たに作成する必要があります。"
   },
+  "not_creatable_page": {
+    "could_not_creata_path": "パスを作成できませんでした。"
+  },
   "custom_navigation": {
     "no_page_list": "このページの配下にはページが存在しません。",
     "link_sharing_is_disabled": "リンクのシェアは無効化されています"

+ 6 - 0
packages/app/public/static/locales/zh_CN/translation.json

@@ -1,4 +1,7 @@
 {
+  "meta": {
+    "display_name": "简体中文"
+  },
   "Help": "帮助",
   "view": "View",
 	"Edit": "编辑",
@@ -195,6 +198,9 @@
     "page_not_exist": "该页面不存在",
     "page_not_exist_alert": "该页面不存在,请创建一个新页面"
   },
+  "not_creatable_page": {
+    "could_not_creata_path": "无法创建路径"
+  },
   "custom_navigation": {
     "no_page_list": "There are no pages under this page.",
     "link_sharing_is_disabled": "链接共享已被禁用"

+ 2 - 2
packages/app/src/client/app.jsx

@@ -10,7 +10,7 @@ import { Provider } from 'unstated';
 import ContextExtractor from '~/client/services/ContextExtractor';
 import EditorContainer from '~/client/services/EditorContainer';
 import PageContainer from '~/client/services/PageContainer';
-import IdenticalPathPage from '~/components/IdenticalPathPage';
+import { IdenticalPathPage } from '~/components/IdenticalPathPage';
 import PrivateLegacyPages from '~/components/PrivateLegacyPages';
 import loggerFactory from '~/utils/logger';
 import { swrGlobalConfiguration } from '~/utils/swr-utils';
@@ -26,7 +26,7 @@ import MyDraftList from '../components/MyDraftList/MyDraftList';
 import GrowiContextualSubNavigation from '../components/Navbar/GrowiContextualSubNavigation';
 import GrowiSubNavigationSwitcher from '../components/Navbar/GrowiSubNavigationSwitcher';
 import NotFoundPage from '../components/NotFoundPage';
-import Page from '../components/Page';
+import { Page } from '../components/Page';
 import DisplaySwitcher from '../components/Page/DisplaySwitcher';
 import RedirectedAlert from '../components/Page/RedirectedAlert';
 import ShareLinkAlert from '../components/Page/ShareLinkAlert';

+ 2 - 5
packages/app/src/client/base.jsx

@@ -3,7 +3,6 @@ import React from 'react';
 import EventEmitter from 'events';
 
 import AppContainer from '~/client/services/AppContainer';
-import SocketIoContainer from '~/client/services/SocketIoContainer';
 import { DescendantsPageListModal } from '~/components/DescendantsPageListModal';
 import PutbackPageModal from '~/components/PutbackPageModal';
 import ShortcutsModal from '~/components/ShortcutsModal';
@@ -14,8 +13,8 @@ import loggerFactory from '~/utils/logger';
 
 import EmptyTrashModal from '../components/EmptyTrashModal';
 import HotkeysManager from '../components/Hotkeys/HotkeysManager';
-import GrowiNavbar from '../components/Navbar/GrowiNavbar';
-import GrowiNavbarBottom from '../components/Navbar/GrowiNavbarBottom';
+import { GrowiNavbar } from '../components/Navbar/GrowiNavbar';
+import { GrowiNavbarBottom } from '../components/Navbar/GrowiNavbarBottom';
 import PageAccessoriesModal from '../components/PageAccessoriesModal';
 import PageCreateModal from '../components/PageCreateModal';
 import PageDeleteModal from '../components/PageDeleteModal';
@@ -40,8 +39,6 @@ window.interceptorManager = new InterceptorManager();
 
 // create unstated container instance
 const appContainer = new AppContainer();
-// eslint-disable-next-line no-unused-vars
-const socketIoContainer = new SocketIoContainer(appContainer);
 
 appContainer.initApp();
 

+ 0 - 4
packages/app/src/client/legacy/crowi.js

@@ -1,10 +1,6 @@
-const { blinkElem, blinkSectionHeaderAtBoot } = require('../util/blink-section-header');
-
 /* eslint-disable react/jsx-filename-extension */
 require('jquery.cookie');
 
-require('./thirdparty-js/waves');
-
 const Crowi = {};
 
 if (!window) {

+ 4 - 4
packages/app/src/client/models/MarkdownTable.js

@@ -1,6 +1,6 @@
+import csvToMarkdown from 'csv-to-markdown-table';
 import markdownTable from 'markdown-table';
 import stringWidth from 'string-width';
-import csvToMarkdown from 'csv-to-markdown-table';
 
 // https://github.com/markdown-it/markdown-it/blob/d29f421927e93e88daf75f22089a3e732e195bd2/lib/rules_block/table.js#L83
 // https://regex101.com/r/7BN2fR/7
@@ -8,9 +8,6 @@ const tableAlignmentLineRE = /^[-:|][-:|\s]*$/;
 const tableAlignmentLineNegRE = /^[^-:]*$/; // it is need to check to ignore empty row which is matched above RE
 const linePartOfTableRE = /^\|[^\r\n]*|[^\r\n]*\|$|([^|\r\n]+\|[^|\r\n]*)+/; // own idea
 
-// set up DOMParser
-const domParser = new (window.DOMParser)();
-
 const defaultOptions = { stringLength: stringWidth };
 
 /**
@@ -67,6 +64,9 @@ export default class MarkdownTable {
    * The error message is a innerHTML, so must not assign it into element.innerHTML because it can lead to Mutation-based XSS
    */
   static fromHTMLTableTag(str) {
+    // set up DOMParser
+    const domParser = new (window.DOMParser)();
+
     // use DOMParser to prevent DOM based XSS (https://developer.mozilla.org/en-US/docs/Web/API/DOMParser)
     const dom = domParser.parseFromString(str, 'application/xml');
 

+ 1 - 2
packages/app/src/client/plugin.js

@@ -8,12 +8,11 @@ export default class GrowiPlugin {
    * process plugin entry
    *
    * @param {AppContainer} appContainer
-   * @param {GrowiRenderer} originRenderer The origin instance of GrowiRenderer
    *
    * @memberof CrowiPlugin
    */
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
-  installAll(appContainer, originRenderer) {
+  installAll(appContainer) {
     // import plugin definitions
     let definitions = [];
     try {

+ 1 - 24
packages/app/src/client/services/AdminSocketIoContainer.js

@@ -1,25 +1,2 @@
-import SocketIoContainer from './SocketIoContainer';
-import { toastError } from '../util/apiNotification';
 
-/**
- * A subclass of SocketIoContainer for /admin namespace
- */
-export default class AdminSocketIoContainer extends SocketIoContainer {
-
-  constructor(appContainer) {
-    super(appContainer, '/admin');
-
-    // show toastr
-    this.socket.on('error', (error) => {
-      toastError(new Error(error));
-    });
-  }
-
-  /**
-   * Workaround for the mangling in production build to break constructor.name
-   */
-  static getClassName() {
-    return 'AdminSocketIoContainer';
-  }
-
-}
+export default class AdminSocketIoContainer {}

+ 12 - 35
packages/app/src/client/services/AppContainer.js

@@ -1,7 +1,5 @@
 import { Container } from 'unstated';
 
-
-import GrowiRenderer from '../util/GrowiRenderer';
 import { i18nFactory } from '../util/i18n';
 
 /**
@@ -26,7 +24,6 @@ export default class AppContainer extends Container {
 
     this.containerInstances = {};
     this.componentInstances = {};
-    this.rendererInstances = {};
   }
 
   /**
@@ -45,8 +42,6 @@ export default class AppContainer extends Container {
 
     this.isDocSaved = true;
 
-    this.originRenderer = new GrowiRenderer(this);
-
     const isPluginEnabled = body.dataset.pluginEnabled === 'true';
     if (isPluginEnabled) {
       this.initPlugins();
@@ -57,19 +52,23 @@ export default class AppContainer extends Container {
 
   initPlugins() {
     const growiPlugin = window.growiPlugin;
-    growiPlugin.installAll(this, this.originRenderer);
+    growiPlugin.installAll(this);
   }
 
   injectToWindow() {
-    window.appContainer = this;
+    // for fix lint error
+
+    // window.appContainer = this;
+
+    // const growiRenderer = new GrowiRenderer(this.getConfig());
+    // growiRenderer.init();
 
-    const originRenderer = this.getOriginRenderer();
-    window.growiRenderer = originRenderer;
+    // window.growiRenderer = growiRenderer;
 
-    // backward compatibility
-    window.crowi = this;
-    window.crowiRenderer = originRenderer;
-    window.crowiPlugin = window.growiPlugin;
+    // // backward compatibility
+    // window.crowi = this;
+    // window.crowiRenderer = window.growiRenderer;
+    // window.crowiPlugin = window.growiPlugin;
   }
 
   getConfig() {
@@ -126,26 +125,4 @@ export default class AppContainer extends Container {
     return this.componentInstances[id];
   }
 
-  getOriginRenderer() {
-    return this.originRenderer;
-  }
-
-  /**
-   * factory method
-   */
-  getRenderer(mode) {
-    if (this.rendererInstances[mode] != null) {
-      return this.rendererInstances[mode];
-    }
-
-    const renderer = new GrowiRenderer(this, this.originRenderer);
-    // setup
-    renderer.initMarkdownItConfigurers(mode);
-    renderer.setup(mode);
-    // register
-    this.rendererInstances[mode] = renderer;
-
-    return renderer;
-  }
-
 }

+ 35 - 1
packages/app/src/client/services/ContextExtractor.tsx

@@ -3,7 +3,9 @@ import React, { FC, useEffect, useState } from 'react';
 
 import { pagePathUtils } from '@growi/core';
 
+import { CustomWindow } from '~/interfaces/global';
 import { IUserUISettings } from '~/interfaces/user-ui-settings';
+// import { generatePreviewRenderer } from '~/services/renderer/growi-renderer';
 import {
   useIsDeviceSmallerThanMd, useIsDeviceSmallerThanLg,
   usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed, useCurrentSidebarContents, useCurrentProductNavWidth,
@@ -20,7 +22,7 @@ import {
   useIsSearchPage, useIsForbidden, useIsIdenticalPath, useHasParent,
   useIsAclEnabled, useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsEnabledAttachTitleHeader,
   useDefaultIndentSize, useIsIndentSizeForced, useCsrfToken, useGrowiVersion, useAuditLogEnabled,
-  useActivityExpirationSeconds, useAuditLogAvailableActions,
+  useActivityExpirationSeconds, useAuditLogAvailableActions, useRendererConfig,
 } from '../../stores/context';
 
 const { isTrashPage: _isTrashPage } = pagePathUtils;
@@ -117,6 +119,20 @@ const ContextExtractorOnce: FC = () => {
   useActivityExpirationSeconds(configByContextHydrate.activityExpirationSeconds);
   useAuditLogAvailableActions(configByContextHydrate.auditLogAvailableActions);
   useGrowiVersion(configByContextHydrate.crowi.version);
+  useRendererConfig({
+    isEnabledLinebreaks: configByContextHydrate.isEnabledLinebreaks,
+    isEnabledLinebreaksInComments: configByContextHydrate.isEnabledLinebreaksInComments,
+    adminPreferredIndentSize: configByContextHydrate.adminPreferredIndentSize,
+    isIndentSizeForced: configByContextHydrate.isIndentSizeForced,
+
+    isEnabledXssPrevention: configByContextHydrate.isEnabledXssPrevention,
+    attrWhiteList: configByContextHydrate.attrWhiteList,
+    tagWhiteList: configByContextHydrate.tagWhiteList,
+    highlightJsStyleBorder: configByContextHydrate.highlightJsStyleBorder,
+
+    plantumlUri: configByContextHydrate.env.PLANTUML_URI,
+    blockdiagUri: configByContextHydrate.env.BLOCKDIAG_URI,
+  });
 
   // Page
   useDeleteUsername(deleteUsername);
@@ -166,6 +182,24 @@ const ContextExtractorOnce: FC = () => {
   const shouldInitAdminSock = !!currentUser?.isAdmin;
   useSetupGlobalAdminSocket(shouldInitAdminSock);
 
+  // TODO: Remove this code when reveal.js is omitted. see: https://github.com/weseek/growi/pull/6223
+  // Do not access this property from other than reveal.js plugins.
+  // (window as CustomWindow).previewRenderer = generatePreviewRenderer(
+  //   {
+  //     isEnabledXssPrevention: configByContextHydrate.isEnabledXssPrevention,
+  //     attrWhiteList: configByContextHydrate.attrWhiteList,
+  //     tagWhiteList: configByContextHydrate.tagWhiteList,
+  //     highlightJsStyleBorder: configByContextHydrate.highlightJsStyleBorder,
+  //     env: {
+  //       MATHJAX: configByContextHydrate.env.MATHJAX,
+  //       PLANTUML_URI: configByContextHydrate.env.PLANTUML_URI,
+  //       BLOCKDIAG_URI: configByContextHydrate.env.BLOCKDIAG_URI,
+  //     },
+  //   },
+  //   null,
+  //   path,
+  // );
+
   return null;
 };
 

+ 2 - 2
packages/app/src/client/services/PageContainer.js

@@ -13,10 +13,10 @@ import { apiv3Post } from '../util/apiv3-client';
 import {
   DetachCodeBlockInterceptor,
   RestoreCodeBlockInterceptor,
-} from '../util/interceptor/detach-code-blocks';
+} from '../../services/renderer/interceptor/detach-code-blocks';
 import {
   DrawioInterceptor,
-} from '../util/interceptor/drawio-interceptor';
+} from '../../services/renderer/interceptor/drawio-interceptor';
 
 const { isTrashPage } = pagePathUtils;
 

+ 0 - 208
packages/app/src/client/util/GrowiRenderer.js

@@ -1,208 +0,0 @@
-import MarkdownIt from 'markdown-it';
-
-import loggerFactory from '~/utils/logger';
-
-import CsvToTable from './PreProcessor/CsvToTable';
-import EasyGrid from './PreProcessor/EasyGrid';
-import Linker from './PreProcessor/Linker';
-import XssFilter from './PreProcessor/XssFilter';
-import BlockdiagConfigurer from './markdown-it/blockdiag';
-import DrawioViewerConfigurer from './markdown-it/drawio-viewer';
-import EmojiConfigurer from './markdown-it/emoji';
-import FooternoteConfigurer from './markdown-it/footernote';
-import HeaderConfigurer from './markdown-it/header';
-import HeaderLineNumberConfigurer from './markdown-it/header-line-number';
-import HeaderWithEditLinkConfigurer from './markdown-it/header-with-edit-link';
-import LinkerByRelativePathConfigurer from './markdown-it/link-by-relative-path';
-import MathJaxConfigurer from './markdown-it/mathjax';
-import PlantUMLConfigurer from './markdown-it/plantuml';
-import TableConfigurer from './markdown-it/table';
-import TableWithHandsontableButtonConfigurer from './markdown-it/table-with-handsontable-button';
-import TaskListsConfigurer from './markdown-it/task-lists';
-import TocAndAnchorConfigurer from './markdown-it/toc-and-anchor';
-
-const logger = loggerFactory('growi:util:GrowiRenderer');
-
-export default class GrowiRenderer {
-
-  /**
-   *
-   * @param {AppContainer} appContainer
-   * @param {GrowiRenderer} originRenderer
-   * @param {string} mode
-   */
-  constructor(appContainer, originRenderer) {
-    this.appContainer = appContainer;
-
-    if (originRenderer != null) {
-      this.preProcessors = originRenderer.preProcessors;
-      this.postProcessors = originRenderer.postProcessors;
-    }
-    else {
-      this.preProcessors = [
-        new EasyGrid(),
-        new Linker(),
-        new CsvToTable(),
-        new XssFilter(appContainer),
-      ];
-      this.postProcessors = [
-      ];
-    }
-
-    this.initMarkdownItConfigurers = this.initMarkdownItConfigurers.bind(this);
-    this.setup = this.setup.bind(this);
-    this.process = this.process.bind(this);
-    this.codeRenderer = this.codeRenderer.bind(this);
-  }
-
-  initMarkdownItConfigurers(mode) {
-    const appContainer = this.appContainer;
-
-    // init markdown-it
-    this.md = new MarkdownIt({
-      html: true,
-      linkify: true,
-      highlight: this.codeRenderer,
-    });
-
-    this.isMarkdownItConfigured = false;
-
-    this.markdownItConfigurers = [
-      new LinkerByRelativePathConfigurer(appContainer),
-      new TaskListsConfigurer(appContainer),
-      new HeaderConfigurer(),
-      new EmojiConfigurer(),
-      new MathJaxConfigurer(appContainer),
-      new DrawioViewerConfigurer(),
-      new PlantUMLConfigurer(appContainer),
-      new BlockdiagConfigurer(appContainer),
-    ];
-
-    // add configurers according to mode
-    switch (mode) {
-      case 'page': {
-        this.markdownItConfigurers = this.markdownItConfigurers.concat([
-          new FooternoteConfigurer(),
-          new TocAndAnchorConfigurer(),
-          new HeaderLineNumberConfigurer(),
-          new HeaderWithEditLinkConfigurer(),
-          new TableWithHandsontableButtonConfigurer(),
-        ]);
-        break;
-      }
-      case 'editor':
-        this.markdownItConfigurers = this.markdownItConfigurers.concat([
-          new FooternoteConfigurer(),
-          new HeaderLineNumberConfigurer(),
-          new TableConfigurer(),
-        ]);
-        break;
-      // case 'comment':
-      //   break;
-      default:
-        this.markdownItConfigurers = this.markdownItConfigurers.concat([
-          new TableConfigurer(),
-        ]);
-        break;
-    }
-  }
-
-  /**
-   * setup with crowi config
-   */
-  setup(mode) {
-    const crowiConfig = this.appContainer.config;
-
-    let isEnabledLinebreaks;
-    switch (mode) {
-      case 'comment':
-        isEnabledLinebreaks = crowiConfig.isEnabledLinebreaksInComments;
-        break;
-      default:
-        isEnabledLinebreaks = crowiConfig.isEnabledLinebreaks;
-        break;
-    }
-
-    this.md.set({
-      breaks: isEnabledLinebreaks,
-    });
-
-    if (!this.isMarkdownItConfigured) {
-      this.markdownItConfigurers.forEach((configurer) => {
-        configurer.configure(this.md);
-      });
-    }
-  }
-
-  preProcess(markdown, context) {
-    let processed = markdown;
-    for (let i = 0; i < this.preProcessors.length; i++) {
-      if (!this.preProcessors[i].process) {
-        continue;
-      }
-      processed = this.preProcessors[i].process(processed, context);
-    }
-
-    return processed;
-  }
-
-  process(markdown, context) {
-    return this.md.render(markdown, context);
-  }
-
-  postProcess(html, context) {
-    let processed = html;
-    for (let i = 0; i < this.postProcessors.length; i++) {
-      if (!this.postProcessors[i].process) {
-        continue;
-      }
-      processed = this.postProcessors[i].process(processed, context);
-    }
-
-    return processed;
-  }
-
-  codeRenderer(code, langExt) {
-    const config = this.appContainer.getConfig();
-    const noborder = (!config.highlightJsStyleBorder) ? 'hljs-no-border' : '';
-
-    let citeTag = '';
-    let hljsLang = 'plaintext';
-    let showLinenumbers = false;
-
-    if (langExt) {
-      // https://regex101.com/r/qGs7eZ/3
-      const match = langExt.match(/^([^:=\n]+)?(=([^:=\n]*))?(:([^:=\n]*))?(=([^:=\n]*))?$/);
-
-      const lang = match[1];
-      const fileName = match[5] || null;
-      showLinenumbers = (match[2] != null) || (match[6] != null);
-
-      if (fileName != null) {
-        citeTag = `<cite>${fileName}</cite>`;
-      }
-      if (hljs.getLanguage(lang)) {
-        hljsLang = lang;
-      }
-    }
-
-    let highlightCode = code;
-    try {
-      highlightCode = hljs.highlight(hljsLang, code, true).value;
-
-      // add line numbers
-      if (showLinenumbers) {
-        highlightCode = hljs.lineNumbersValue((highlightCode));
-      }
-    }
-    catch (err) {
-      logger.error(err);
-    }
-
-    return `<pre class="hljs ${noborder}">${citeTag}<code>${highlightCode}</code></pre>`;
-  }
-
-  highlightCode(code, lang) {
-  }
-
-}

+ 0 - 23
packages/app/src/client/util/PreProcessor/XssFilter.js

@@ -1,23 +0,0 @@
-import Xss from '~/services/xss';
-import XssOption from '~/services/xss/xssOption';
-
-export default class XssFilter {
-
-  constructor(crowi) {
-    this.crowi = crowi;
-
-    if (crowi.config.isEnabledXssPrevention) {
-      this.xssOption = new XssOption(crowi.config);
-      this.xss = new Xss(this.xssOption);
-    }
-  }
-
-  process(markdown) {
-    if (this.crowi.config.isEnabledXssPrevention) {
-      return this.xss.process(markdown);
-    }
-
-    return markdown;
-  }
-
-}

+ 1 - 0
packages/app/src/client/util/blink-section-header.ts

@@ -23,5 +23,6 @@ export const blinkSectionHeaderAtBoot = (): HTMLElement | undefined => {
   const elem = document.getElementById(id);
   if (elem != null && elem.tagName.match(/h\d+/i)) { // match h1, h2, h3...
     blinkElem(elem);
+    return elem;
   }
 };

+ 4 - 0
packages/app/src/client/util/i18n.js

@@ -1,3 +1,4 @@
+
 /* eslint-disable */
 import i18n from 'i18next';
 import LanguageDetector from 'i18next-browser-languagedetector';
@@ -15,6 +16,9 @@ Object.values(locales).forEach((locale) => {
   });
 });
 
+/*
+* Note: This file will be deleted. use "~/next-i18next.config" instead
+*/
 // extract metadata list from 'public/static/locales/${locale}/meta.json'
 export const localeMetadatas = Object.values(locales).map(locale => locale.meta);
 

+ 0 - 17
packages/app/src/client/util/markdown-it/blockdiag.js

@@ -1,17 +0,0 @@
-export default class BlockdiagConfigurer {
-
-  constructor(crowi) {
-    this.crowi = crowi;
-    const config = crowi.getConfig();
-
-    this.generateSourceUrl = config.env.BLOCKDIAG_URI || 'https://blockdiag-api.com/';
-  }
-
-  configure(md) {
-    md.use(require('markdown-it-blockdiag'), {
-      generateSourceUrl: this.generateSourceUrl,
-      marker: ':::',
-    });
-  }
-
-}

+ 0 - 9
packages/app/src/client/util/markdown-it/drawio-viewer.js

@@ -1,9 +0,0 @@
-export default class DrawioViewerConfigurer {
-
-  configure(md) {
-    md.use(require('markdown-it-drawio-viewer'), {
-      marker: ':::',
-    });
-  }
-
-}

+ 0 - 12
packages/app/src/client/util/markdown-it/emoji.js

@@ -1,12 +0,0 @@
-import markdownItEmojiMart from 'markdown-it-emoji-mart';
-
-import { emojiMartData } from './emoji-mart-data';
-
-
-export default class EmojiConfigurer {
-
-  configure(md) {
-    md.use(markdownItEmojiMart, { defs: emojiMartData });
-  }
-
-}

+ 0 - 16
packages/app/src/client/util/markdown-it/mathjax.js

@@ -1,16 +0,0 @@
-export default class MathJaxConfigurer {
-
-  constructor(crowi) {
-    this.crowi = crowi;
-
-    const config = crowi.getConfig();
-    this.isEnabled = !!config.env.MATHJAX; // convert to boolean
-  }
-
-  configure(md) {
-    if (this.isEnabled) {
-      md.use(require('markdown-it-mathjax')());
-    }
-  }
-
-}

+ 0 - 18
packages/app/src/client/util/markdown-it/task-lists.js

@@ -1,18 +0,0 @@
-export default class TaskListsConfigurer {
-
-  constructor(crowi) {
-    this.crowi = crowi;
-  }
-
-  configure(md) {
-    md.use(require('markdown-it-task-checkbox'), {
-      disabled: true,
-      divWrap: true,
-      divClass: 'checkbox checkbox-primary',
-      idPrefix: 'cbx_',
-      ulClass: 'task-list',
-      liClass: 'task-list-item',
-    });
-  }
-
-}

+ 0 - 27
packages/app/src/client/util/markdown-it/toc-and-anchor.js

@@ -1,27 +0,0 @@
-import markdownItEmojiMart from 'markdown-it-emoji-mart';
-import markdownItToc from 'markdown-it-toc-and-anchor-with-slugid';
-
-import { emojiMartData } from './emoji-mart-data';
-
-export default class TocAndAnchorConfigurer {
-
-  configure(md) {
-    md.use(markdownItEmojiMart, { defs: emojiMartData })
-      .use(markdownItToc, {
-        tocLastLevel: 3,
-        anchorLinkBefore: false,
-        anchorLinkSymbol: '',
-        anchorLinkSymbolClassName: 'icon-link',
-        anchorClassName: 'revision-head-link',
-      });
-
-    // set toc render function
-    md.set({
-      tocCallback: (tocMarkdown, tocArray, tocHtml) => {
-        // eslint-disable-next-line no-undef
-        globalEmitter.emit('renderTocHtml', tocHtml);
-      },
-    });
-  }
-
-}

+ 4 - 7
packages/app/src/client/util/reveal/plugins/growi-renderer.js

@@ -2,19 +2,16 @@
  * reveal.js growi-renderer plugin.
  */
 (function(root, factory) {
-  // get AppContainer instance from parent window
-  const appContainer = window.parent.appContainer;
-
-  const growiRendererPlugin = factory(appContainer);
+  const growiRendererPlugin = factory();
   growiRendererPlugin.initialize();
-}(this, (appContainer) => {
+}(this, () => {
   /* eslint-disable no-useless-escape */
   const DEFAULT_SLIDE_SEPARATOR = '^\r?\n---\r?\n$';
   const DEFAULT_ELEMENT_ATTRIBUTES_SEPARATOR = '\\\.element\\\s*?(.+?)$';
   const DEFAULT_SLIDE_ATTRIBUTES_SEPARATOR = '\\\.slide:\\\s*?(\\\S.+?)$';
   /* eslint-enable no-useless-escape */
 
-  const growiRenderer = appContainer.getRenderer('editor');
+  const growiRenderer = window.parent.previewRenderer;
 
   let marked;
 
@@ -61,7 +58,7 @@
         section.setAttribute('data-markdown-parsed', 'true');
         const notes = section.querySelector('aside.notes');
         markdown = marked.getMarkdownFromSlide(section);
-        const context = { markdown };
+        const context = { markdown, currentPathname: decodeURIComponent(window.parent.location.pathname) };
 
         interceptorManager.process('preRender', context)
           .then(() => { return interceptorManager.process('prePreProcess', context) })

+ 23 - 18
packages/app/src/components/Admin/App/AppSetting.jsx

@@ -1,11 +1,11 @@
 import React, { useCallback } from 'react';
 
+import { useTranslation, i18n } from 'next-i18next';
 import PropTypes from 'prop-types';
-import { useTranslation } from 'next-i18next';
 
 import AdminAppContainer from '~/client/services/AdminAppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
-import { localeMetadatas } from '~/client/util/i18n';
+import { i18n as i18nConfig } from '~/next-i18next.config';
 import loggerFactory from '~/utils/logger';
 
 
@@ -77,22 +77,27 @@ const AppSetting = (props) => {
         </label>
         <div className="col-md-6 py-2">
           {
-            localeMetadatas.map(meta => (
-              <div key={meta.id} className="custom-control custom-radio custom-control-inline">
-                <input
-                  type="radio"
-                  id={`radioLang${meta.id}`}
-                  className="custom-control-input"
-                  name="globalLang"
-                  value={meta.id}
-                  checked={adminAppContainer.state.globalLang === meta.id}
-                  onChange={(e) => {
-                    adminAppContainer.changeGlobalLang(e.target.value);
-                  }}
-                />
-                <label className="custom-control-label" htmlFor={`radioLang${meta.id}`}>{meta.displayName}</label>
-              </div>
-            ))
+            i18nConfig.locales.map((locale) => {
+              const fixedT = i18n.getFixedT(locale);
+              i18n.loadLanguages(i18nConfig.locales);
+
+              return (
+                <div key={locale} className="custom-control custom-radio custom-control-inline">
+                  <input
+                    type="radio"
+                    id={`radioLang${locale}`}
+                    className="custom-control-input"
+                    name="globalLang"
+                    value={locale}
+                    checked={adminAppContainer.state.globalLang === locale}
+                    onChange={(e) => {
+                      adminAppContainer.changeGlobalLang(e.target.value);
+                    }}
+                  />
+                  <label className="custom-control-label" htmlFor={`radioLang${locale}`}>{fixedT('meta.display_name')}</label>
+                </div>
+              );
+            })
           }
         </div>
       </div>

+ 1 - 2
packages/app/src/components/Admin/App/AppSettingsPageContents.tsx

@@ -58,8 +58,7 @@ const AppSettingsPageContents = (props: Props) => {
       <div className="row">
         <div className="col-lg-12">
           <h2 className="admin-setting-header">{t('App Settings')}</h2>
-          {/* TODO: show AppSetting by https://redmine.weseek.co.jp/issues/100056 */}
-          {/* <AppSetting /> */}
+          <AppSetting />
         </div>
       </div>
 

+ 45 - 51
packages/app/src/components/Admin/ExportArchiveDataPage.jsx

@@ -1,17 +1,13 @@
-import React, { Fragment } from 'react';
+import React from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 import * as toastr from 'toastr';
 
 
-import AdminSocketIoContainer from '~/client/services/AdminSocketIoContainer';
-import AppContainer from '~/client/services/AppContainer';
-import { apiDelete, apiGet } from '~/client/util/apiv1-client';
-
-import { withUnstatedContainers } from '../UnstatedUtils';
 // import { toastSuccess, toastError } from '~/client/util/apiNotification';
-
+import { apiDelete, apiGet } from '~/client/util/apiv1-client';
+import { useAdminSocket } from '~/stores/socket-io';
 
 import LabeledProgressBar from './Common/LabeledProgressBar';
 import ArchiveFilesTable from './ExportArchiveData/ArchiveFilesTable';
@@ -70,45 +66,47 @@ class ExportArchiveDataPage extends React.Component {
   }
 
   setupWebsocketEventHandler() {
-    const socket = this.props.adminSocketIoContainer.getSocket();
-
-    // websocket event
-    socket.on('admin:onProgressForExport', ({ currentCount, totalCount, progressList }) => {
-      this.setState({
-        isExporting: true,
-        progressList,
-      });
-    });
-
-    // websocket event
-    socket.on('admin:onStartZippingForExport', () => {
-      this.setState({
-        isZipping: true,
+    const { socket } = this.props;
+
+    if (socket != null) {
+      // websocket event
+      socket.on('admin:onProgressForExport', ({ currentCount, totalCount, progressList }) => {
+        this.setState({
+          isExporting: true,
+          progressList,
+        });
       });
-    });
-
-    // websocket event
-    socket.on('admin:onTerminateForExport', ({ addedZipFileStat }) => {
-      const zipFileStats = this.state.zipFileStats.concat([addedZipFileStat]);
 
-      this.setState({
-        isExporting: false,
-        isZipping: false,
-        isExported: true,
-        zipFileStats,
+      // websocket event
+      socket.on('admin:onStartZippingForExport', () => {
+        this.setState({
+          isZipping: true,
+        });
       });
 
-      // TODO: toastSuccess, toastError
-      toastr.success(undefined, `New Archive Data '${addedZipFileStat.fileName}' is added`, {
-        closeButton: true,
-        progressBar: true,
-        newestOnTop: false,
-        showDuration: '100',
-        hideDuration: '100',
-        timeOut: '1200',
-        extendedTimeOut: '150',
+      // websocket event
+      socket.on('admin:onTerminateForExport', ({ addedZipFileStat }) => {
+        const zipFileStats = this.state.zipFileStats.concat([addedZipFileStat]);
+
+        this.setState({
+          isExporting: false,
+          isZipping: false,
+          isExported: true,
+          zipFileStats,
+        });
+
+        // TODO: toastSuccess, toastError
+        toastr.success(undefined, `New Archive Data '${addedZipFileStat.fileName}' is added`, {
+          closeButton: true,
+          progressBar: true,
+          newestOnTop: false,
+          showDuration: '100',
+          hideDuration: '100',
+          timeOut: '1200',
+          extendedTimeOut: '150',
+        });
       });
-    });
+    }
   }
 
   onZipFileStatAdd(newStat) {
@@ -250,18 +248,14 @@ class ExportArchiveDataPage extends React.Component {
 
 ExportArchiveDataPage.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminSocketIoContainer: PropTypes.instanceOf(AdminSocketIoContainer).isRequired,
+  socket: PropTypes.object,
 };
 
 const ExportArchiveDataPageWrapperFC = (props) => {
   const { t } = useTranslation();
-  return <ExportArchiveDataPage t={t} {...props} />;
-};
+  const { data: socket } = useAdminSocket();
 
-/**
- * Wrapper component for using unstated
- */
-const ExportArchiveDataPageWrapper = withUnstatedContainers(ExportArchiveDataPageWrapperFC, [AppContainer, AdminSocketIoContainer]);
+  return <ExportArchiveDataPage t={t} socket={socket} {...props} />;
+};
 
-export default ExportArchiveDataPageWrapper;
+export default ExportArchiveDataPageWrapperFC;

+ 4 - 6
packages/app/src/components/Admin/MarkdownSetting/MarkDownSettingContents.tsx

@@ -5,8 +5,8 @@ import { Card, CardBody } from 'reactstrap';
 
 import IndentForm from './IndentForm';
 import LineBreakForm from './LineBreakForm';
-// import PresentationForm from './PresentationForm';
-// import XssForm from './XssForm';
+import PresentationForm from './PresentationForm';
+import XssForm from './XssForm';
 
 const MarkDownSettingContents = React.memo((): JSX.Element => {
   const { t } = useTranslation();
@@ -32,16 +32,14 @@ const MarkDownSettingContents = React.memo((): JSX.Element => {
       <Card className="card well my-3">
         <CardBody className="px-0 py-2">{ t('admin:markdown_setting.presentation_desc') }</CardBody>
       </Card>
-      {/* TODO: show PresentationForm and Xss Form components by https://redmine.weseek.co.jp/issues/100058 */}
-      {/* <PresentationForm /> */}
+      <PresentationForm />
 
       {/* XSS Setting */}
       <h2 className="admin-setting-header">{ t('admin:markdown_setting.xss_header') }</h2>
       <Card className="card well my-3">
         <CardBody className="px-0 py-2">{ t('admin:markdown_setting.xss_desc') }</CardBody>
       </Card>
-      {/* TODO: show PresentationForm and Xss Form components by https://redmine.weseek.co.jp/issues/100058 */}
-      {/* <XssForm /> */}
+      <XssForm />
     </div>
   );
 });

+ 2 - 4
packages/app/src/components/Admin/MarkdownSetting/PresentationForm.jsx

@@ -1,10 +1,9 @@
 import React from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import loggerFactory from '~/utils/logger';
 
@@ -129,7 +128,6 @@ class PresentationForm extends React.Component {
 
 PresentationForm.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
 
 };
@@ -140,6 +138,6 @@ const PresentationFormWrapperFC = (props) => {
   return <PresentationForm t={t} {...props} />;
 };
 
-const PresentationFormWrapper = withUnstatedContainers(PresentationFormWrapperFC, [AppContainer, AdminMarkDownContainer]);
+const PresentationFormWrapper = withUnstatedContainers(PresentationFormWrapperFC, [AdminMarkDownContainer]);
 
 export default PresentationFormWrapper;

+ 2 - 4
packages/app/src/components/Admin/MarkdownSetting/WhiteListInput.jsx

@@ -1,10 +1,9 @@
 import React from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { tags, attrs } from '~/services/xss/recommended-whitelist';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -79,7 +78,6 @@ class WhiteListInput extends React.Component {
 
 WhiteListInput.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
 
 };
@@ -90,6 +88,6 @@ const PresentationFormWrapperFC = (props) => {
   return <WhiteListInput t={t} {...props} />;
 };
 
-const WhiteListWrapper = withUnstatedContainers(PresentationFormWrapperFC, [AppContainer, AdminMarkDownContainer]);
+const WhiteListWrapper = withUnstatedContainers(PresentationFormWrapperFC, [AdminMarkDownContainer]);
 
 export default WhiteListWrapper;

+ 2 - 4
packages/app/src/components/Admin/MarkdownSetting/XssForm.jsx

@@ -1,10 +1,9 @@
 import React from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { tags, attrs } from '~/services/xss/recommended-whitelist';
 import loggerFactory from '~/utils/logger';
@@ -165,7 +164,6 @@ class XssForm extends React.Component {
 
 XssForm.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
 };
 
@@ -175,6 +173,6 @@ const XssFormWrapperFC = (props) => {
   return <XssForm t={t} {...props} />;
 };
 
-const XssFormWrapper = withUnstatedContainers(XssFormWrapperFC, [AppContainer, AdminMarkDownContainer]);
+const XssFormWrapper = withUnstatedContainers(XssFormWrapperFC, [AdminMarkDownContainer]);
 
 export default XssFormWrapper;

+ 2 - 4
packages/app/src/components/Admin/Notification/GlobalNotification.jsx

@@ -1,10 +1,9 @@
 import React from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import loggerFactory from '~/utils/logger';
 
@@ -130,7 +129,6 @@ class GlobalNotification extends React.Component {
 
 GlobalNotification.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer).isRequired,
 
 };
@@ -141,6 +139,6 @@ const GlobalNotificationWrapperFC = (props) => {
   return <GlobalNotification t={t} {...props} />;
 };
 
-const GlobalNotificationWrapper = withUnstatedContainers(GlobalNotificationWrapperFC, [AppContainer, AdminNotificationContainer]);
+const GlobalNotificationWrapper = withUnstatedContainers(GlobalNotificationWrapperFC, [AdminNotificationContainer]);
 
 export default GlobalNotificationWrapper;

+ 2 - 4
packages/app/src/components/Admin/Notification/GlobalNotificationList.jsx

@@ -1,11 +1,10 @@
 import React from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 import urljoin from 'url-join';
 
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Put } from '~/client/util/apiv3-client';
 import loggerFactory from '~/utils/logger';
@@ -176,7 +175,6 @@ class GlobalNotificationList extends React.Component {
 
 GlobalNotificationList.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer).isRequired,
 
 };
@@ -187,6 +185,6 @@ const GlobalNotificationListWrapperFC = (props) => {
   return <GlobalNotificationList t={t} {...props} />;
 };
 
-const GlobalNotificationListWrapper = withUnstatedContainers(GlobalNotificationListWrapperFC, [AppContainer, AdminNotificationContainer]);
+const GlobalNotificationListWrapper = withUnstatedContainers(GlobalNotificationListWrapperFC, [AdminNotificationContainer]);
 
 export default GlobalNotificationListWrapper;

+ 9 - 12
packages/app/src/components/Admin/Notification/NotificationSetting.jsx

@@ -1,28 +1,25 @@
 import React, {
   useCallback, useEffect, useMemo, useState,
 } from 'react';
-import PropTypes from 'prop-types';
 
+import { SlackbotType } from '@growi/slack';
+import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 import {
   TabContent, TabPane,
 } from 'reactstrap';
-import { useTranslation } from 'next-i18next';
-
-import { SlackbotType } from '@growi/slack';
-
-import loggerFactory from '~/utils/logger';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
+import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
 import { toastError } from '~/client/util/apiNotification';
 import { toArrayIfNot } from '~/utils/array-utils';
-import { withLoadingSppiner } from '../../SuspenseUtils';
-
-import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
+import loggerFactory from '~/utils/logger';
 
 import { CustomNavTab } from '../../CustomNavigation/CustomNav';
+import { withUnstatedContainers } from '../../UnstatedUtils';
+
 
-import UserTriggerNotification from './UserTriggerNotification';
 import GlobalNotification from './GlobalNotification';
+import UserTriggerNotification from './UserTriggerNotification';
 
 const logger = loggerFactory('growi:NotificationSetting');
 
@@ -174,7 +171,7 @@ function NotificationSetting(props) {
   );
 }
 
-const NotificationSettingWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(NotificationSetting), [AdminNotificationContainer]);
+const NotificationSettingWithUnstatedContainer = withUnstatedContainers(NotificationSetting, [AdminNotificationContainer]);
 
 NotificationSetting.propTypes = {
   adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer).isRequired,

+ 2 - 4
packages/app/src/components/Admin/Notification/UserNotificationRow.jsx

@@ -1,10 +1,9 @@
 import React from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
-import AppContainer from '~/client/services/AppContainer';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
@@ -38,7 +37,6 @@ class UserNotificationRow extends React.PureComponent {
 
 UserNotificationRow.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer).isRequired,
 
   notification: PropTypes.object.isRequired,
@@ -52,7 +50,7 @@ const UserNotificationRowWrapperWrapperFC = (props) => {
   return <UserNotificationRow t={t} {...props} />;
 };
 
-const UserNotificationRowWrapper = withUnstatedContainers(UserNotificationRowWrapperWrapperFC, [AppContainer, AdminNotificationContainer]);
+const UserNotificationRowWrapper = withUnstatedContainers(UserNotificationRowWrapperWrapperFC, [AdminNotificationContainer]);
 
 
 export default UserNotificationRowWrapper;

+ 2 - 4
packages/app/src/components/Admin/Notification/UserTriggerNotification.jsx

@@ -1,10 +1,9 @@
 import React from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import loggerFactory from '~/utils/logger';
 
@@ -148,7 +147,6 @@ class UserTriggerNotification extends React.Component {
 
 UserTriggerNotification.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer).isRequired,
 
 };
@@ -159,6 +157,6 @@ const UserTriggerNotificationWrapperFC = (props) => {
   return <UserTriggerNotification t={t} {...props} />;
 };
 
-const UserTriggerNotificationWrapper = withUnstatedContainers(UserTriggerNotificationWrapperFC, [AppContainer, AdminNotificationContainer]);
+const UserTriggerNotificationWrapper = withUnstatedContainers(UserTriggerNotificationWrapperFC, [AdminNotificationContainer]);
 
 export default UserTriggerNotificationWrapper;

+ 5 - 5
packages/app/src/components/Admin/Security/FacebookSecuritySetting.jsx

@@ -1,12 +1,13 @@
 /* eslint-disable react/no-danger */
 import React from 'react';
-import PropTypes from 'prop-types';
+
 import { withTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
+
+import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
-import AppContainer from '~/client/services/AppContainer';
-import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 
 class FacebookSecurityManagement extends React.Component {
 
@@ -30,10 +31,9 @@ class FacebookSecurityManagement extends React.Component {
 
 FacebookSecurityManagement.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
 };
 
-const TwitterSecurityManagementWrapper = withUnstatedContainers(FacebookSecurityManagement, [AppContainer, AdminGeneralSecurityContainer]);
+const TwitterSecurityManagementWrapper = withUnstatedContainers(FacebookSecurityManagement, [AdminGeneralSecurityContainer]);
 
 export default withTranslation()(TwitterSecurityManagementWrapper);

+ 21 - 21
packages/app/src/components/Admin/Security/SecurityManagementContents.jsx

@@ -1,21 +1,21 @@
-import React, { Fragment, useMemo, useState } from 'react';
+import React, { useMemo, useState } from 'react';
 
 import { useTranslation } from 'next-i18next';
 import { TabContent, TabPane } from 'reactstrap';
 
 import CustomNav from '../../CustomNavigation/CustomNav';
 
-import BasicSecuritySetting from './BasicSecuritySetting';
-import FacebookSecuritySetting from './FacebookSecuritySetting';
-import GitHubSecuritySetting from './GitHubSecuritySetting';
-import GoogleSecuritySetting from './GoogleSecuritySetting';
-import LdapSecuritySetting from './LdapSecuritySetting';
-import LocalSecuritySetting from './LocalSecuritySetting';
-import OidcSecuritySetting from './OidcSecuritySetting';
-import SamlSecuritySetting from './SamlSecuritySetting';
+// import BasicSecuritySetting from './BasicSecuritySetting';
+// import FacebookSecuritySetting from './FacebookSecuritySetting';
+// import GitHubSecuritySetting from './GitHubSecuritySetting';
+// import GoogleSecuritySetting from './GoogleSecuritySetting';
+// import LdapSecuritySetting from './LdapSecuritySetting';
+// import LocalSecuritySetting from './LocalSecuritySetting';
+// import OidcSecuritySetting from './OidcSecuritySetting';
+// import SamlSecuritySetting from './SamlSecuritySetting';
 import SecuritySetting from './SecuritySetting';
 import ShareLinkSetting from './ShareLinkSetting';
-import TwitterSecuritySetting from './TwitterSecuritySetting';
+// import TwitterSecuritySetting from './TwitterSecuritySetting';
 
 const SecurityManagementContents = () => {
   const { t } = useTranslation();
@@ -82,12 +82,12 @@ const SecurityManagementContents = () => {
   return (
     <div data-testid="admin-security">
       <div className="mb-5">
-        <SecuritySetting />
+        {/* <SecuritySetting /> */}
       </div>
 
       {/* Shared Link List */}
       <div className="mb-5">
-        <ShareLinkSetting />
+        {/* <ShareLinkSetting /> */}
       </div>
 
 
@@ -112,31 +112,31 @@ const SecurityManagementContents = () => {
         />
         <TabContent activeTab={activeTab} className="p-5">
           <TabPane tabId="passport_local">
-            {activeComponents.has('passport_local') && <LocalSecuritySetting />}
+            {/* {activeComponents.has('passport_local') && <LocalSecuritySetting />} */}
           </TabPane>
           <TabPane tabId="passport_ldap">
-            {activeComponents.has('passport_ldap') && <LdapSecuritySetting />}
+            {/* {activeComponents.has('passport_ldap') && <LdapSecuritySetting />} */}
           </TabPane>
           <TabPane tabId="passport_saml">
-            {activeComponents.has('passport_saml') && <SamlSecuritySetting />}
+            {/* {activeComponents.has('passport_saml') && <SamlSecuritySetting />} */}
           </TabPane>
           <TabPane tabId="passport_oidc">
-            {activeComponents.has('passport_oidc') && <OidcSecuritySetting />}
+            {/* {activeComponents.has('passport_oidc') && <OidcSecuritySetting />} */}
           </TabPane>
           <TabPane tabId="passport_basic">
-            {activeComponents.has('passport_basic') && <BasicSecuritySetting />}
+            {/* {activeComponents.has('passport_basic') && <BasicSecuritySetting />} */}
           </TabPane>
           <TabPane tabId="passport_google">
-            {activeComponents.has('passport_google') && <GoogleSecuritySetting />}
+            {/* {activeComponents.has('passport_google') && <GoogleSecuritySetting />} */}
           </TabPane>
           <TabPane tabId="passport_github">
-            {activeComponents.has('passport_github') && <GitHubSecuritySetting />}
+            {/* {activeComponents.has('passport_github') && <GitHubSecuritySetting />} */}
           </TabPane>
           <TabPane tabId="passport_twitter">
-            {activeComponents.has('passport_twitter') && <TwitterSecuritySetting />}
+            {/* {activeComponents.has('passport_twitter') && <TwitterSecuritySetting />} */}
           </TabPane>
           <TabPane tabId="passport_facebook">
-            {activeComponents.has('passport_facebook') && <FacebookSecuritySetting />}
+            {/* {activeComponents.has('passport_facebook') && <FacebookSecuritySetting />} */}
           </TabPane>
         </TabContent>
       </div>

+ 4 - 6
packages/app/src/components/Admin/Security/ShareLinkSetting.jsx

@@ -1,10 +1,9 @@
 import React, { Fragment } from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Delete } from '~/client/util/apiv3-client';
 
@@ -78,7 +77,7 @@ class ShareLinkSetting extends React.Component {
   }
 
   async deleteAllLinksButtonHandler() {
-    const { t, appContainer } = this.props;
+    const { t } = this.props;
 
     try {
       const res = await apiv3Delete('/share-links/all');
@@ -92,7 +91,7 @@ class ShareLinkSetting extends React.Component {
   }
 
   async deleteLinkById(shareLinkId) {
-    const { t, appContainer, adminGeneralSecurityContainer } = this.props;
+    const { t, adminGeneralSecurityContainer } = this.props;
     const { shareLinksActivePage } = adminGeneralSecurityContainer.state;
 
     try {
@@ -193,7 +192,6 @@ class ShareLinkSetting extends React.Component {
 
 ShareLinkSetting.propTypes = {
   t: PropTypes.func.isRequired, //  i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
 };
 
@@ -205,6 +203,6 @@ const ShareLinkSettingWrapperFC = (props) => {
 /**
  * Wrapper component for using unstated
  */
-const ShareLinkSettingWrapper = withUnstatedContainers(ShareLinkSettingWrapperFC, [AppContainer, AdminGeneralSecurityContainer]);
+const ShareLinkSettingWrapper = withUnstatedContainers(ShareLinkSettingWrapperFC, [AdminGeneralSecurityContainer]);
 
 export default ShareLinkSettingWrapper;

+ 18 - 0
packages/app/src/components/BookmarkButtons.module.scss

@@ -0,0 +1,18 @@
+@use '~/styles/bootstrap/init' as bs;
+
+.btn-bookmark {
+  :global {
+    box-shadow: none !important;
+  }
+
+  &:global {
+    @include bs.button-outline-variant(rgba(bs.$secondary, 50%), bs.$orange, rgba(lighten(bs.$orange, 20%), 0.5), rgba(lighten(bs.$orange, 20%), 0.5));
+    &:not(:disabled):not(.disabled):active,
+    &:not(:disabled):not(.disabled).active {
+      color: bs.$orange;
+    }
+    &:not(:disabled):not(.disabled):not(:hover) {
+      background-color: transparent;
+    }
+  }
+}

+ 3 - 1
packages/app/src/components/BookmarkButtons.tsx

@@ -9,6 +9,8 @@ import { IUser } from '../interfaces/user';
 
 import UserPictureList from './User/UserPictureList';
 
+import styles from './BookmarkButtons.module.scss';
+
 interface Props {
   bookmarkCount?: number
   isBookmarked?: boolean
@@ -55,7 +57,7 @@ const BookmarkButtons: FC<Props> = (props: Props) => {
         type="button"
         id="bookmark-button"
         onClick={handleClick}
-        className={`shadow-none btn btn-bookmark border-0
+        className={`shadow-none btn ${styles['btn-bookmark']} border-0
           ${isBookmarked ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`}
       >
         <i className={`fa ${isBookmarked ? 'fa-bookmark' : 'fa-bookmark-o'}`}></i>

+ 17 - 0
packages/app/src/components/LikeButtons.module.scss

@@ -0,0 +1,17 @@
+@use '~/styles/bootstrap/init' as bs;
+
+.btn-like {
+  :global {
+    box-shadow: none !important;
+  }
+  &:global {
+    @include bs.button-outline-variant(rgba(bs.$secondary, 50%), lighten(bs.$red, 15%), rgba(lighten(bs.$red, 10%), 0.15), rgba(lighten(bs.$red, 10%), 0.5));
+    &:not(:disabled):not(.disabled):active,
+    &:not(:disabled):not(.disabled).active {
+      color: lighten(bs.$red, 15%);
+    }
+    &:not(:disabled):not(.disabled):not(:hover) {
+      background-color: transparent;
+    }
+  }
+}

+ 4 - 15
packages/app/src/components/LikeButtons.tsx

@@ -3,13 +3,12 @@ import React, { FC, useState, useCallback } from 'react';
 import { useTranslation } from 'next-i18next';
 import { UncontrolledTooltip, Popover, PopoverBody } from 'reactstrap';
 
-import AppContainer from '~/client/services/AppContainer';
-
 import { IUser } from '../interfaces/user';
 
-import { withUnstatedContainers } from './UnstatedUtils';
 import UserPictureList from './User/UserPictureList';
 
+import styles from './LikeButtons.module.scss';
+
 type LikeButtonsProps = {
 
   hideTotalNumber?: boolean,
@@ -51,7 +50,7 @@ const LikeButtons: FC<LikeButtonsProps> = (props: LikeButtonsProps) => {
         type="button"
         id="like-button"
         onClick={onLikeClicked}
-        className={`shadow-none btn btn-like border-0
+        className={`shadow-none btn ${styles['btn-like']} border-0
             ${isLiked ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`}
       >
         <i className={`fa ${isLiked ? 'fa-heart' : 'fa-heart-o'}`}></i>
@@ -85,14 +84,4 @@ const LikeButtons: FC<LikeButtonsProps> = (props: LikeButtonsProps) => {
 
 };
 
-/**
- * Wrapper component for using unstated
- */
-const LikeButtonsUnstatedWrapper = withUnstatedContainers(LikeButtons, [AppContainer]);
-
-// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-const LikeButtonsWrapper = (props) => {
-  return <LikeButtonsUnstatedWrapper {...props}></LikeButtonsUnstatedWrapper>;
-};
-
-export default LikeButtonsWrapper;
+export default LikeButtons;

+ 0 - 215
packages/app/src/components/MyDraftList/Draft.jsx

@@ -1,215 +0,0 @@
-import React from 'react';
-
-import PropTypes from 'prop-types';
-import { CopyToClipboard } from 'react-copy-to-clipboard';
-import { useTranslation } from 'next-i18next';
-import {
-  Collapse,
-  UncontrolledTooltip,
-} from 'reactstrap';
-
-import AppContainer from '~/client/services/AppContainer';
-
-import RevisionBody from '../Page/RevisionBody';
-import { withUnstatedContainers } from '../UnstatedUtils';
-
-class Draft extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      html: '',
-      isRendered: false,
-      isPanelExpanded: false,
-      showCopiedMessage: false,
-    };
-
-    this.growiRenderer = this.props.appContainer.getRenderer('draft');
-
-    this.changeToolTipLabel = this.changeToolTipLabel.bind(this);
-    this.expandPanelHandler = this.expandPanelHandler.bind(this);
-    this.collapsePanelHandler = this.collapsePanelHandler.bind(this);
-    this.renderHtml = this.renderHtml.bind(this);
-    this.renderAccordionTitle = this.renderAccordionTitle.bind(this);
-  }
-
-  changeToolTipLabel() {
-    this.setState({ showCopiedMessage: true });
-    setTimeout(() => {
-      this.setState({ showCopiedMessage: false });
-    }, 1000);
-  }
-
-  expandPanelHandler() {
-    this.setState({ isPanelExpanded: true });
-
-    if (!this.state.isRendered) {
-      this.renderHtml();
-    }
-  }
-
-  collapsePanelHandler() {
-    this.setState({ isPanelExpanded: false });
-  }
-
-  async renderHtml() {
-    const context = {
-      markdown: this.props.markdown,
-    };
-
-    const growiRenderer = this.growiRenderer;
-    const { interceptorManager } = window;
-    await interceptorManager.process('prePreProcess', context)
-      .then(() => {
-        context.markdown = growiRenderer.preProcess(context.markdown, context);
-      })
-      .then(() => { return interceptorManager.process('postPreProcess', context) })
-      .then(() => {
-        const parsedHTML = growiRenderer.process(context.markdown, context);
-        context.parsedHTML = parsedHTML;
-      })
-      .then(() => { return interceptorManager.process('prePostProcess', context) })
-      .then(() => {
-        context.parsedHTML = growiRenderer.postProcess(context.parsedHTML, context);
-      })
-      .then(() => { return interceptorManager.process('postPostProcess', context) })
-      .then(() => {
-        this.setState({ html: context.parsedHTML, isRendered: true });
-      });
-  }
-
-  renderAccordionTitle(isExist) {
-    const { isPanelExpanded } = this.state;
-    const { t } = this.props;
-    const iconClass = isPanelExpanded ? 'fa-rotate-90' : '';
-
-    return (
-      <span>
-
-        <span className="mr-2 draft-path" onClick={() => this.setState({ isPanelExpanded: !isPanelExpanded })}>
-          <i className={`fa fa-fw fa-angle-right mr-2 ${iconClass}`}></i>
-          {this.props.path}
-        </span>
-        { isExist && (
-          <span className="badge badge-warning">{t('page exists')}</span>
-        ) }
-        { !isExist && (
-          <span className="badge badge-info">draft</span>
-        ) }
-
-        <a className="ml-2" href={this.props.path}><i className="icon icon-login"></i></a>
-      </span>
-    );
-  }
-
-  renderControls() {
-    const { t, path, index } = this.props;
-
-    const tooltipTargetId = `draft-copied-tooltip_${index}`;
-
-    return (
-      <div className="icon-container">
-        {this.props.isExist
-          ? null
-          : (
-            <a
-              href={`${path}#edit`}
-              target="_blank"
-              rel="noopener noreferrer"
-              data-toggle="tooltip"
-              title={this.props.t('Edit')}
-            >
-              <i className="mx-2 icon-note" />
-            </a>
-          )
-        }
-        <span id={tooltipTargetId}>
-          <CopyToClipboard text={this.props.markdown} onCopy={this.changeToolTipLabel}>
-            <a
-              className="text-center draft-copy"
-            >
-              <i className="mx-2 ti-clipboard" />
-            </a>
-          </CopyToClipboard>
-        </span>
-        <UncontrolledTooltip placement="top" target={tooltipTargetId} fade={false} trigger="hover">
-          { this.state.showCopiedMessage && (
-            <strong>copied!</strong>
-          ) }
-          { !this.state.showCopiedMessage && (
-            <span>{this.props.t('Copy')}</span>
-          ) }
-        </UncontrolledTooltip>
-        <a
-          className="text-danger text-center"
-          data-toggle="tooltip"
-          data-placement="top"
-          title={t('Delete')}
-          onClick={() => { return this.props.clearDraft(this.props.path) }}
-        >
-          <i className="mx-2 icon-trash" />
-        </a>
-      </div>
-    );
-  }
-
-  render() {
-    const { isPanelExpanded } = this.state;
-
-    return (
-      <div className="accordion draft-list-item" role="tablist">
-        <div className="card">
-
-          <div className="card-header d-flex" role="tab">
-            {this.renderAccordionTitle(this.props.isExist)}
-
-            <div className="flex-grow-1"></div>
-
-            {this.renderControls()}
-          </div>
-
-          <Collapse isOpen={isPanelExpanded} onEntering={this.expandPanelHandler} onExiting={this.collapsePanelHandler}>
-            <div className="card-body">
-              {/* loading spinner */}
-              { this.state.isPanelExpanded && !this.state.isRendered && (
-                <div className="text-center">
-                  <i className="fa fa-lg fa-spinner fa-pulse mx-auto text-muted"></i>
-                </div>
-              ) }
-              {/* contents */}
-              { this.state.isPanelExpanded && this.state.isRendered && (
-                <RevisionBody html={this.state.html} />
-              ) }
-            </div>
-          </Collapse>
-
-        </div>
-      </div>
-    );
-  }
-
-}
-
-Draft.propTypes = {
-  t: PropTypes.func.isRequired,
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-
-  index: PropTypes.number.isRequired,
-  path: PropTypes.string.isRequired,
-  markdown: PropTypes.string.isRequired,
-  isExist: PropTypes.bool.isRequired,
-  clearDraft: PropTypes.func.isRequired,
-};
-
-const DraftWrapperFC = (props) => {
-  const { t } = useTranslation();
-  return <Draft t={t} {...props} />;
-};
-
-/**
- * Wrapper component for using unstated
- */
-const DraftWrapper = withUnstatedContainers(DraftWrapperFC, [AppContainer]);
-
-export default DraftWrapper;

+ 144 - 0
packages/app/src/components/MyDraftList/Draft.tsx

@@ -0,0 +1,144 @@
+import React, { useState } from 'react';
+
+import { useTranslation } from 'next-i18next';
+import { CopyToClipboard } from 'react-copy-to-clipboard';
+import ReactMarkdown from 'react-markdown';
+import {
+  Collapse,
+  UncontrolledTooltip,
+} from 'reactstrap';
+
+import { useDraftOptions } from '~/stores/renderer';
+
+type DraftProps = {
+  path: string,
+  isExist: boolean,
+  index: number,
+  markdown: string,
+  clearDraft: (path: string) => void,
+}
+
+export const Draft = (props: DraftProps): JSX.Element => {
+
+  const {
+    path, isExist, index, markdown, clearDraft,
+  } = props;
+  const { t } = useTranslation();
+  const { data: rendererOptions } = useDraftOptions();
+  const [isPanelExpanded, setIsPanelExpanded] = useState(false);
+  const [showCopiedMessage, setShowCopiedMessage] = useState(false);
+
+  const changeToolTipLabel = () => {
+    setShowCopiedMessage(true);
+    setTimeout(() => {
+      setShowCopiedMessage(false);
+    }, 1000);
+  };
+
+  const collapsePanelHandler = () => {
+    setIsPanelExpanded(false);
+  };
+
+  const expandPanelHandler = () => {
+    setIsPanelExpanded(true);
+  };
+
+  const Controls = () => {
+
+    const tooltipTargetId = `draft-copied-tooltip_${index}`;
+
+    return (
+      <div className="icon-container">
+        {isExist
+          ? null
+          : (
+            <a
+              href={`${path}#edit`}
+              target="_blank"
+              rel="noopener noreferrer"
+              data-toggle="tooltip"
+              title={t('Edit')}
+            >
+              <i className="mx-2 icon-note" />
+            </a>
+          )
+        }
+        <span id={tooltipTargetId}>
+          <CopyToClipboard text={markdown} onCopy={changeToolTipLabel}>
+            <a
+              className="text-center draft-copy"
+            >
+              <i className="mx-2 ti-clipboard" />
+            </a>
+          </CopyToClipboard>
+        </span>
+        <UncontrolledTooltip placement="top" target={tooltipTargetId} fade={false} trigger="hover">
+          { showCopiedMessage && (
+            <strong>copied!</strong>
+          ) }
+          { !showCopiedMessage && (
+            <span>{t('Copy')}</span>
+          ) }
+        </UncontrolledTooltip>
+        <a
+          className="text-danger text-center"
+          data-toggle="tooltip"
+          data-placement="top"
+          title={t('Delete')}
+          onClick={() => { return clearDraft(path) }}
+        >
+          <i className="mx-2 icon-trash" />
+        </a>
+      </div>
+    );
+  };
+
+  const AccordionTitle = () => {
+    const iconClass = isPanelExpanded ? 'fa-rotate-90' : '';
+
+    return (
+      <span>
+
+        <span className="mr-2 draft-path" onClick={() => setIsPanelExpanded(!isPanelExpanded)}>
+          <i className={`fa fa-fw fa-angle-right mr-2 ${iconClass}`}></i>
+          {path}
+        </span>
+        { isExist && (
+          <span className="badge badge-warning">{t('page exists')}</span>
+        ) }
+        { !isExist && (
+          <span className="badge badge-info">draft</span>
+        ) }
+
+        <a className="ml-2" href={path}><i className="icon icon-login"></i></a>
+      </span>
+    );
+  };
+
+
+  return (
+    <div className="accordion draft-list-item" role="tablist">
+      <div className="card">
+
+        <div className="card-header d-flex" role="tab">
+          <AccordionTitle/>
+
+          <div className="flex-grow-1"></div>
+
+          <Controls/>
+        </div>
+
+        <Collapse isOpen={isPanelExpanded} onEntering={expandPanelHandler} onExiting={collapsePanelHandler}>
+          <div className="card-body">
+            { isPanelExpanded && (
+              <ReactMarkdown {...rendererOptions} className='wiki'>
+                {markdown}
+              </ReactMarkdown>
+            ) }
+          </div>
+        </Collapse>
+
+      </div>
+    </div>
+  );
+};

+ 1 - 1
packages/app/src/components/MyDraftList/MyDraftList.jsx

@@ -10,7 +10,7 @@ import { apiGet } from '~/client/util/apiv1-client';
 import PaginationWrapper from '../PaginationWrapper';
 import { withUnstatedContainers } from '../UnstatedUtils';
 
-import Draft from './Draft';
+import { Draft } from './Draft';
 
 class MyDraftList extends React.Component {
 

+ 34 - 47
packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -4,16 +4,13 @@ import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import { DropdownItem } from 'reactstrap';
 
-import EditorContainer from '~/client/services/EditorContainer';
-import PageContainer from '~/client/services/PageContainer';
 import { exportAsMarkdown } from '~/client/services/page-operation';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiPost } from '~/client/util/apiv1-client';
-import { getIdForRef } from '~/interfaces/common';
+import { isPopulated } from '~/interfaces/common';
 import {
   IPageToRenameWithMeta, IPageWithMeta, IPageInfoForEntity,
 } from '~/interfaces/page';
-import { IResTagsUpdateApiv1 } from '~/interfaces/tag';
 import { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
 import {
   useCurrentPageId,
@@ -26,7 +23,7 @@ import {
 } from '~/stores/modal';
 import { useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
 import {
-  EditorMode, useDrawerMode, useEditorMode, useIsDeviceSmallerThanMd, useIsAbleToShowPageManagement, useIsAbleToShowTagLabel,
+  EditorMode, useDrawerMode, useEditorMode, useIsAbleToShowPageManagement, useIsAbleToShowTagLabel,
   useIsAbleToShowPageEditorModeManager, useIsAbleToShowPageAuthors,
 } from '~/stores/ui';
 
@@ -36,7 +33,6 @@ import AttachmentIcon from '../Icons/AttachmentIcon';
 import HistoryIcon from '../Icons/HistoryIcon';
 import PresentationIcon from '../Icons/PresentationIcon';
 import ShareLinkIcon from '../Icons/ShareLinkIcon';
-import { withUnstatedContainers } from '../UnstatedUtils';
 
 
 import { GrowiSubNavigation } from './GrowiSubNavigation';
@@ -44,7 +40,7 @@ import PageEditorModeManager from './PageEditorModeManager';
 import { SubNavButtons } from './SubNavButtons';
 
 
-type AdditionalMenuItemsProps = AdditionalMenuItemsRendererProps & {
+type AdditionalMenuItemsProps = {
   pageId: string,
   revisionId: string,
   isLinkSharingDisabled?: boolean,
@@ -149,13 +145,22 @@ const AdditionalMenuItems = (props: AdditionalMenuItemsProps): JSX.Element => {
   );
 };
 
+type GrowiContextualSubNavigationProps = {
+  isCompactMode?: boolean,
+  isLinkSharingDisabled: boolean,
+};
+
+const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps): JSX.Element => {
+
+  const { data: currentPage, mutate: mutateCurrentPage } = useSWRxCurrentPage();
+  const path = currentPage?.path;
+
+  const revision = currentPage?.revision;
+  const revisionId = (revision != null && isPopulated(revision)) ? revision._id : undefined;
 
-const GrowiContextualSubNavigation = (props) => {
-  const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
   const { data: isDrawerMode } = useDrawerMode();
   const { data: editorMode, mutate: mutateEditorMode } = useEditorMode();
   const { data: pageId } = useCurrentPageId();
-  const { data: currentPage } = useSWRxCurrentPage();
   const { data: currentUser } = useCurrentUser();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isSharedUser } = useIsSharedUser();
@@ -194,9 +199,7 @@ const GrowiContextualSubNavigation = (props) => {
 
   const [isPageTemplateModalShown, setIsPageTempleteModalShown] = useState(false);
 
-  const {
-    isCompactMode, isLinkSharingDisabled, pageContainer,
-  } = props;
+  const { isCompactMode, isLinkSharingDisabled } = props;
 
   const isViewMode = editorMode === EditorMode.View;
 
@@ -208,9 +211,8 @@ const GrowiContextualSubNavigation = (props) => {
 
     const { _id: pageId, revision: revisionId } = currentPage;
     try {
-      const res: IResTagsUpdateApiv1 = await apiPost('/tags.update', { pageId, revisionId, tags: newTags });
-      const updatedRevisionId = getIdForRef(res.savedPage.revision);
-      await pageContainer.setState({ revisionId: updatedRevisionId });
+      await apiPost('/tags.update', { pageId, revisionId, tags: newTags });
+      mutateCurrentPage();
 
       // revalidate SWRTagsInfo
       mutateSWRTagsInfo();
@@ -222,7 +224,7 @@ const GrowiContextualSubNavigation = (props) => {
       toastError(err, 'fail to update tags');
     }
 
-  }, [currentPage, mutateSWRTagsInfo, mutatePageTagsForEditors, pageContainer]);
+  }, [mutateSWRTagsInfo, mutatePageTagsForEditors, mutateCurrentPage, pageId, revisionId]);
 
   const tagsUpdatedHandlerForEditMode = useCallback((newTags: string[]): void => {
     // It will not be reflected in the DB until the page is refreshed
@@ -274,7 +276,7 @@ const GrowiContextualSubNavigation = (props) => {
 
 
   const ControlComponents = useCallback(() => {
-    if (currentPage == null) {
+    if (currentPage == null || pageId == null) {
       return <></>;
     }
 
@@ -282,20 +284,19 @@ const GrowiContextualSubNavigation = (props) => {
       mutateEditorMode(viewType);
     }
 
-    const { _id: pageId, revision, path } = currentPage;
-
-    let additionalMenuItemsRenderer;
-    if (revision != null) {
-      additionalMenuItemsRenderer = props => function additionalMenuItemsRenderer() {
-        return (<AdditionalMenuItems
-          {...props}
+    const additionalMenuItemsRenderer = () => {
+      if (revisionId == null || pageId == null) {
+        return <></>;
+      }
+      return (
+        <AdditionalMenuItems
           pageId={pageId}
-          revisionId={revision}
+          revisionId={revisionId}
           isLinkSharingDisabled={isLinkSharingDisabled}
           onClickTemplateMenuItem={templateMenuItemClickHandler}
-        />);
-      };
-    }
+        />
+      );
+    };
     return (
       <>
         <div className="d-flex flex-column align-items-end justify-content-center py-md-2" style={{ gap: `${isCompactMode ? '5px' : '7px'}` }}>
@@ -305,8 +306,8 @@ const GrowiContextualSubNavigation = (props) => {
               <SubNavButtons
                 isCompactMode={isCompactMode}
                 pageId={pageId}
+                revisionId={revisionId}
                 shareLinkId={shareLinkId}
-                revisionId={revision.toString()}
                 path={path}
                 disableSeenUserInfoPopover={isSharedUser}
                 showPageControlDropdown={isAbleToShowPageManagement}
@@ -322,7 +323,6 @@ const GrowiContextualSubNavigation = (props) => {
               onPageEditorModeButtonClicked={onPageEditorModeButtonClicked}
               isBtnDisabled={isGuestUser}
               editorMode={editorMode}
-              isDeviceSmallerThanMd={isDeviceSmallerThanMd}
             />
           )}
         </div>
@@ -336,8 +336,8 @@ const GrowiContextualSubNavigation = (props) => {
       </>
     );
   }, [
-    currentPage, shareLinkId, editorMode, mutateEditorMode, isCompactMode,
-    isLinkSharingDisabled, isDeviceSmallerThanMd, isGuestUser, isSharedUser, currentUser,
+    pageId, revisionId, editorMode, mutateEditorMode, isCompactMode,
+    isLinkSharingDisabled, isGuestUser, isSharedUser, currentUser,
     isViewMode, isAbleToShowPageEditorModeManager, isAbleToShowPageManagement,
     duplicateItemClickedHandler, renameItemClickedHandler, deleteItemClickedHandler,
     templateMenuItemClickHandler, isPageTemplateModalShown,
@@ -364,18 +364,5 @@ const GrowiContextualSubNavigation = (props) => {
   );
 };
 
-/**
- * Wrapper component for using unstated
- */
-const GrowiContextualSubNavigationWrapper = withUnstatedContainers(GrowiContextualSubNavigation, [EditorContainer, PageContainer]);
-
-
-GrowiContextualSubNavigation.propTypes = {
-  editorContainer: PropTypes.instanceOf(EditorContainer).isRequired,
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
-
-  isCompactMode: PropTypes.bool,
-  isLinkSharingDisabled: PropTypes.bool,
-};
 
-export default GrowiContextualSubNavigationWrapper;
+export default GrowiContextualSubNavigation;

+ 6 - 3
packages/app/src/components/Navbar/GrowiNavbar.tsx

@@ -5,6 +5,7 @@ import React, {
 import { isServer } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
+import Link from 'next/link';
 import { useRipple } from 'react-use-ripple';
 import { UncontrolledTooltip } from 'reactstrap';
 
@@ -141,9 +142,11 @@ export const GrowiNavbar = (): JSX.Element => {
     <nav id="grw-navbar" className={`navbar grw-navbar ${styles['grw-navbar']} navbar-expand navbar-dark sticky-top mb-0 px-0`}>
       {/* Brand Logo  */}
       <div className="navbar-brand mr-0">
-        <a className="grw-logo d-block" href="/">
-          <GrowiLogo />
-        </a>
+        <Link href="/">
+          <a className="grw-logo d-block">
+            <GrowiLogo />
+          </a>
+        </Link>
       </div>
 
       <div className="grw-app-title d-none d-md-block">

+ 199 - 0
packages/app/src/components/Navbar/GrowiSubNavigation.module.scss

@@ -0,0 +1,199 @@
+@use '~/styles/variables' as var;
+@use '~/styles/bootstrap/init' as bs;
+@use '~/styles/mixins';
+
+// https://github.com/css-modules/css-modules/issues/295#issuecomment-404873976
+// workaround to use '&' in global scope
+.grw-subnav {
+  :global {
+    min-height: var.$grw-subnav-min-height;
+    padding-top: 8px;
+    padding-bottom: 8px;
+
+    @include bs.media-breakpoint-up(md) {
+      min-height: var.$grw-subnav-min-height-md;
+    }
+
+    .grw-drawer-toggler {
+      width: 50px;
+      height: 50px;
+      font-size: 24px;
+    }
+
+    h1 {
+      @include mixins.variable-font-size(32px);
+      line-height: 1.4em;
+    }
+
+    .grw-taglabels-container {
+      margin-bottom: 0.5rem;
+    }
+
+    .grw-page-path-nav {
+      .separator {
+        margin-right: 0.2em;
+        margin-left: 0.2em;
+      }
+    }
+
+    .btn-subscribe {
+      height: 40px;
+      font-size: 20px;
+    }
+
+    .btn-like,
+    .btn-bookmark,
+    .btn-seen-user {
+      height: 40px;
+      padding-right: 6px;
+      padding-left: 8px;
+      font-size: 20px;
+      svg {
+        width: 20px;
+        height: 20px;
+      }
+    }
+    .total-likes,
+    .total-bookmarks {
+      display: flex;
+      align-items: flex-end;
+      padding-right: 8px;
+      padding-left: 6px;
+      font-size: 14px;
+      font-weight: bs.$font-weight-bold;
+    }
+    .seen-user-count {
+      padding-right: 6px;
+      padding-left: 6px;
+      font-size: 14px;
+      font-weight: bs.$font-weight-bold;
+      vertical-align: bottom;
+    }
+
+    .btn-page-item-control {
+      height: 40px;
+      font-size: 16px;
+    }
+
+    ul.authors {
+      li {
+        font-size: 12px;
+        list-style: none;
+      }
+
+      .text-date {
+        font-size: 11px;
+      }
+
+      .picture {
+        width: 22px;
+        height: 22px;
+        border: 1px solid bs.$gray-300;
+
+        &.picture-xs {
+          width: 14px;
+          height: 14px;
+        }
+      }
+
+      .user-list-popover {
+        max-width: 200px;
+
+        .user-list-content {
+          direction: rtl;
+
+          .liker-user-count,
+          .seen-user-count {
+            font-size: 12px;
+            font-weight: bolder;
+          }
+        }
+        .cls-1 {
+          isolation: isolate;
+        }
+      }
+    }
+  }
+
+  &:global {
+    &:hover {
+      .btn-copy,
+      .btn-edit,
+      .btn-edit-tags {
+        // change button opacity
+        opacity: unset;
+      }
+    }
+
+    /*
+     * Compact Mode
+     */
+    &.grw-subnav-compact {
+      min-height: 70px;
+
+      @include bs.media-breakpoint-up(md) {
+        min-height: 90px;
+      }
+
+      .btn-like,
+      .btn-bookmark,
+      .btn-subscribe {
+        width: 32px;
+        height: 32px;
+        padding: 4px;
+        font-size: 16px;
+      }
+      .btn-seen-user {
+        width: 48px;
+        height: 32px;
+        padding: 4px;
+        font-size: 16px;
+
+        svg {
+          width: 16px;
+          height: 16px;
+        }
+      }
+      .btn-page-item-control {
+        width: 32px;
+        height: 32px;
+        font-size: 12px;
+      }
+    }
+  }
+}
+
+/*
+ * shadow
+ */
+.grw-subnav-append-shadow-container {
+  .grw-subnav {
+    box-shadow: 0px 0px 6px 3px rgba(black, 0.15);
+  }
+}
+
+/*
+ * Fixed ver
+ */
+$easeInOutCubic: cubic-bezier(0.65, 0, 0.35, 1);
+
+.grw-subnav-fixed-container {
+  top: var.$grw-navbar-border-width;
+  z-index: bs.$zindex-sticky - 5;
+}
+
+/*
+ * Switching show/hide
+ */
+.grw-subnav-switcher {
+  .grw-subnav-fixed-container {
+    transition: transform 150ms $easeInOutCubic;
+  }
+
+  &.grw-subnav-switcher-hidden {
+    .grw-subnav-fixed-container {
+      transition: unset;
+      transform: translateY(-100%);
+    }
+  }
+}

+ 9 - 2
packages/app/src/components/Navbar/GrowiSubNavigation.tsx

@@ -1,5 +1,7 @@
 import React from 'react';
 
+import dynamic from 'next/dynamic';
+
 import { IPageHasId } from '~/interfaces/page';
 import { IUser } from '~/interfaces/user';
 import {
@@ -7,11 +9,13 @@ import {
 } from '~/stores/ui';
 
 import TagLabels from '../Page/TagLabels';
-import PagePathNav from '../PagePathNav';
+// import PagePathNav from '../PagePathNav';
 
 import AuthorInfo from './AuthorInfo';
 import DrawerToggler from './DrawerToggler';
 
+import styles from './GrowiSubNavigation.module.scss';
+
 
 type Props = {
   page: Partial<IPageHasId>,
@@ -32,6 +36,9 @@ type Props = {
 }
 
 export const GrowiSubNavigation = (props: Props): JSX.Element => {
+
+  const PagePathNav = dynamic(() => import('../PagePathNav'), { ssr: false });
+
   const { data: editorMode } = useEditorMode();
 
   const {
@@ -57,7 +64,7 @@ export const GrowiSubNavigation = (props: Props): JSX.Element => {
 
   return (
     <div className={
-      'grw-subnav d-flex align-items-center justify-content-between'
+      `grw-subnav ${styles['grw-subnav']} d-flex align-items-center justify-content-between`
       + ` ${additionalClasses.join(' ')}`
       + ` ${isCompactMode ? 'grw-subnav-compact d-print-none' : ''}`}
     >

+ 7 - 15
packages/app/src/components/Navbar/PageEditorModeManager.jsx

@@ -4,11 +4,10 @@ import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import { UncontrolledTooltip } from 'reactstrap';
 
-import AppContainer from '~/client/services/AppContainer';
-import { useCurrentUser } from '~/stores/context';
+import { useCurrentUser, useHackmdUri } from '~/stores/context';
 import { EditorMode, useIsDeviceSmallerThanMd } from '~/stores/ui';
 
-import { withUnstatedContainers } from '../UnstatedUtils';
+import styles from './PageEditorModeManager.module.scss';
 
 /* eslint-disable react/prop-types */
 const PageEditorModeButtonWrapper = React.memo(({
@@ -42,16 +41,16 @@ PageEditorModeButtonWrapper.displayName = 'PageEditorModeButtonWrapper';
 
 function PageEditorModeManager(props) {
   const {
-    appContainer,
     editorMode, onPageEditorModeButtonClicked, isBtnDisabled,
   } = props;
 
   const { t } = useTranslation();
   const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
   const { data: currentUser } = useCurrentUser();
+  const { data: hackmdUri } = useHackmdUri();
 
-  const isAdmin = currentUser?.isAdmin;
-  const isHackmdEnabled = appContainer.config.env.HACKMD_URI != null;
+  const isAdmin = currentUser?.admin;
+  const isHackmdEnabled = hackmdUri != null;
   const showHackmdBtn = isHackmdEnabled || isAdmin;
 
   const pageEditorModeButtonClickedHandler = useCallback((viewType) => {
@@ -66,7 +65,7 @@ function PageEditorModeManager(props) {
   return (
     <>
       <div
-        className="btn-group grw-page-editor-mode-manager"
+        className={`btn-group grw-page-editor-mode-manager ${styles['grw-page-editor-mode-manager']}`}
         role="group"
         aria-label="page-editor-mode-manager"
         id="grw-page-editor-mode-manager"
@@ -121,8 +120,6 @@ function PageEditorModeManager(props) {
 }
 
 PageEditorModeManager.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-
   onPageEditorModeButtonClicked: PropTypes.func,
   isBtnDisabled: PropTypes.bool,
   editorMode: PropTypes.string,
@@ -132,9 +129,4 @@ PageEditorModeManager.defaultProps = {
   isBtnDisabled: false,
 };
 
-/**
- * Wrapper component for using unstated
- */
-const PageEditorModeManagerWrapper = withUnstatedContainers(PageEditorModeManager, [AppContainer]);
-
-export default PageEditorModeManagerWrapper;
+export default PageEditorModeManager;

+ 34 - 0
packages/app/src/components/Navbar/PageEditorModeManager.module.scss

@@ -0,0 +1,34 @@
+// @mixin page-editor-mode-manager($textColor, $borderColor, $bgColorHoverAndActive, $bgColor: white) {
+@use '~/styles/bootstrap/init' as bs;
+@use '~/styles/mixins';
+
+.grw-page-editor-mode-manager :global {
+  .btn {
+    width: 70px;
+    white-space: nowrap;
+
+    @include mixins.border-vertical('before', 70%, 1, true);
+
+    &.view-button,
+    &.edit-button {
+      line-height: 1.2rem;
+      .grw-page-editor-mode-manager-icon {
+        @include bs.media-breakpoint-down(sm) {
+          font-size: 1.2rem;
+        }
+      }
+    }
+    &.hackmd-button {
+      line-height: 1.2rem;
+      .grw-page-editor-mode-manager-icon {
+        @include bs.media-breakpoint-down(sm) {
+          font-size: 1.2rem;
+        }
+      }
+      .grw-page-editor-mode-manager-label {
+        font-size: 12px;
+        letter-spacing: -0.6px;
+      }
+    }
+  }
+}

+ 18 - 0
packages/app/src/components/NotCreatablePage.tsx

@@ -0,0 +1,18 @@
+import React, { FC } from 'react';
+
+import { useTranslation } from 'next-i18next';
+
+export const NotCreatablePage: FC = () => {
+  const { t } = useTranslation();
+
+  return (
+    <div className="row not-found-message-row">
+      <div className="col-md-12">
+        <h2 className="text-muted">
+          <i className="icon-ban mr-1" aria-hidden="true"></i>
+          { t('not_creatable_page.could_not_creata_path') }
+        </h2>
+      </div>
+    </div>
+  );
+};

+ 141 - 137
packages/app/src/components/Page.jsx

@@ -1,35 +1,34 @@
-import React, { useEffect, useRef } from 'react';
+import React, {
+  useCallback, useEffect, useMemo, useRef, useState,
+} from 'react';
 
+import dynamic from 'next/dynamic';
 import PropTypes from 'prop-types';
+import { debounce } from 'throttle-debounce';
 
 import MarkdownTable from '~/client/models/MarkdownTable';
-import AppContainer from '~/client/services/AppContainer';
-import EditorContainer from '~/client/services/EditorContainer';
-import PageContainer from '~/client/services/PageContainer';
+import { blinkSectionHeaderAtBoot } from '~/client/util/blink-section-header';
 import { getOptionsToSave } from '~/client/util/editor';
 import {
-  useCurrentPagePath, useIsGuestUser,
+  useIsGuestUser, useIsBlinkedHeaderAtBoot,
 } from '~/stores/context';
 import {
   useSWRxSlackChannels, useIsSlackEnabled, usePageTagsForEditors, useIsEnabledUnsavedWarning,
 } from '~/stores/editor';
+import { useSWRxCurrentPage } from '~/stores/page';
+import { useViewOptions } from '~/stores/renderer';
 import {
-  useEditorMode, useIsMobile, useSelectedGrant, useSelectedGrantGroupId, useSelectedGrantGroupName,
+  useEditorMode, useIsMobile,
 } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 
 import RevisionRenderer from './Page/RevisionRenderer';
-import DrawioModal from './PageEditor/DrawioModal';
-import GridEditModal from './PageEditor/GridEditModal';
-import HandsontableModal from './PageEditor/HandsontableModal';
-import LinkEditModal from './PageEditor/LinkEditModal';
 import mdu from './PageEditor/MarkdownDrawioUtil';
 import mtu from './PageEditor/MarkdownTableUtil';
-import { withUnstatedContainers } from './UnstatedUtils';
 
 const logger = loggerFactory('growi:Page');
 
-class Page extends React.Component {
+class PageSubstance extends React.Component {
 
   constructor(props) {
     super(props);
@@ -39,8 +38,6 @@ class Page extends React.Component {
       currentTargetDrawioArea: null,
     };
 
-    this.growiRenderer = this.props.appContainer.getRenderer('page');
-
     this.gridEditModal = React.createRef();
     this.linkEditModal = React.createRef();
     this.handsontableModal = React.createRef();
@@ -56,10 +53,10 @@ class Page extends React.Component {
    * @param endLineNumber
    */
   launchHandsontableModal(beginLineNumber, endLineNumber) {
-    const markdown = this.props.pageContainer.state.markdown;
-    const tableLines = markdown.split(/\r\n|\r|\n/).slice(beginLineNumber - 1, endLineNumber).join('\n');
-    this.setState({ currentTargetTableArea: { beginLineNumber, endLineNumber } });
-    this.handsontableModal.current.show(MarkdownTable.fromMarkdownString(tableLines));
+    // const markdown = this.props.pageContainer.state.markdown;
+    // const tableLines = markdown.split(/\r\n|\r|\n/).slice(beginLineNumber - 1, endLineNumber).join('\n');
+    // this.setState({ currentTargetTableArea: { beginLineNumber, endLineNumber } });
+    // this.handsontableModal.current.show(MarkdownTable.fromMarkdownString(tableLines));
   }
 
   /**
@@ -68,96 +65,102 @@ class Page extends React.Component {
    * @param endLineNumber
    */
   launchDrawioModal(beginLineNumber, endLineNumber) {
-    const markdown = this.props.pageContainer.state.markdown;
-    const drawioMarkdownArray = markdown.split(/\r\n|\r|\n/).slice(beginLineNumber - 1, endLineNumber);
-    const drawioData = drawioMarkdownArray.slice(1, drawioMarkdownArray.length - 1).join('\n').trim();
-    this.setState({ currentTargetDrawioArea: { beginLineNumber, endLineNumber } });
-    this.drawioModal.current.show(drawioData);
+    // const markdown = this.props.pageContainer.state.markdown;
+    // const drawioMarkdownArray = markdown.split(/\r\n|\r|\n/).slice(beginLineNumber - 1, endLineNumber);
+    // const drawioData = drawioMarkdownArray.slice(1, drawioMarkdownArray.length - 1).join('\n').trim();
+    // this.setState({ currentTargetDrawioArea: { beginLineNumber, endLineNumber } });
+    // this.drawioModal.current.show(drawioData);
   }
 
   async saveHandlerForHandsontableModal(markdownTable) {
-    const {
-      isSlackEnabled, slackChannels, pageContainer, mutateIsEnabledUnsavedWarning, grant, grantGroupId, grantGroupName, pageTags,
-    } = this.props;
-    const optionsToSave = getOptionsToSave(isSlackEnabled, slackChannels, grant, grantGroupId, grantGroupName, pageTags);
-
-    const newMarkdown = mtu.replaceMarkdownTableInMarkdown(
-      markdownTable,
-      this.props.pageContainer.state.markdown,
-      this.state.currentTargetTableArea.beginLineNumber,
-      this.state.currentTargetTableArea.endLineNumber,
-    );
-
-    try {
-      // disable unsaved warning
-      mutateIsEnabledUnsavedWarning(false);
-
-      // eslint-disable-next-line no-unused-vars
-      const { page, tags } = await pageContainer.save(newMarkdown, this.props.editorMode, optionsToSave);
-      logger.debug('success to save');
-
-      pageContainer.showSuccessToastr();
-    }
-    catch (error) {
-      logger.error('failed to save', error);
-      pageContainer.showErrorToastr(error);
-    }
-    finally {
-      this.setState({ currentTargetTableArea: null });
-    }
+    // const {
+    //   isSlackEnabled, slackChannels, pageContainer, mutateIsEnabledUnsavedWarning, grant, grantGroupId, grantGroupName, pageTags,
+    // } = this.props;
+    // const optionsToSave = getOptionsToSave(isSlackEnabled, slackChannels, grant, grantGroupId, grantGroupName, pageTags);
+
+    // const newMarkdown = mtu.replaceMarkdownTableInMarkdown(
+    //   markdownTable,
+    //   this.props.pageContainer.state.markdown,
+    //   this.state.currentTargetTableArea.beginLineNumber,
+    //   this.state.currentTargetTableArea.endLineNumber,
+    // );
+
+    // try {
+    //   // disable unsaved warning
+    //   mutateIsEnabledUnsavedWarning(false);
+
+    //   // eslint-disable-next-line no-unused-vars
+    //   const { page, tags } = await pageContainer.save(newMarkdown, this.props.editorMode, optionsToSave);
+    //   logger.debug('success to save');
+
+    //   pageContainer.showSuccessToastr();
+    // }
+    // catch (error) {
+    //   logger.error('failed to save', error);
+    //   pageContainer.showErrorToastr(error);
+    // }
+    // finally {
+    //   this.setState({ currentTargetTableArea: null });
+    // }
   }
 
   async saveHandlerForDrawioModal(drawioData) {
-    const {
-      isSlackEnabled, slackChannels, pageContainer, pageTags, grant, grantGroupId, grantGroupName, mutateIsEnabledUnsavedWarning,
-    } = this.props;
-    const optionsToSave = getOptionsToSave(isSlackEnabled, slackChannels, grant, grantGroupId, grantGroupName, pageTags);
-
-    const newMarkdown = mdu.replaceDrawioInMarkdown(
-      drawioData,
-      this.props.pageContainer.state.markdown,
-      this.state.currentTargetDrawioArea.beginLineNumber,
-      this.state.currentTargetDrawioArea.endLineNumber,
-    );
-
-    try {
-      // disable unsaved warning
-      mutateIsEnabledUnsavedWarning(false);
-
-      // eslint-disable-next-line no-unused-vars
-      const { page, tags } = await pageContainer.save(newMarkdown, this.props.editorMode, optionsToSave);
-      logger.debug('success to save');
-
-      pageContainer.showSuccessToastr();
-    }
-    catch (error) {
-      logger.error('failed to save', error);
-      pageContainer.showErrorToastr(error);
-    }
-    finally {
-      this.setState({ currentTargetDrawioArea: null });
-    }
+    // const {
+    //   isSlackEnabled, slackChannels, pageContainer, pageTags, grant, grantGroupId, grantGroupName, mutateIsEnabledUnsavedWarning,
+    // } = this.props;
+    // const optionsToSave = getOptionsToSave(isSlackEnabled, slackChannels, grant, grantGroupId, grantGroupName, pageTags);
+
+    // const newMarkdown = mdu.replaceDrawioInMarkdown(
+    //   drawioData,
+    //   this.props.pageContainer.state.markdown,
+    //   this.state.currentTargetDrawioArea.beginLineNumber,
+    //   this.state.currentTargetDrawioArea.endLineNumber,
+    // );
+
+    // try {
+    //   // disable unsaved warning
+    //   mutateIsEnabledUnsavedWarning(false);
+
+    //   // eslint-disable-next-line no-unused-vars
+    //   const { page, tags } = await pageContainer.save(newMarkdown, this.props.editorMode, optionsToSave);
+    //   logger.debug('success to save');
+
+    //   pageContainer.showSuccessToastr();
+    // }
+    // catch (error) {
+    //   logger.error('failed to save', error);
+    //   pageContainer.showErrorToastr(error);
+    // }
+    // finally {
+    //   this.setState({ currentTargetDrawioArea: null });
+    // }
   }
 
   render() {
     const {
-      pageContainer, pagePath, isMobile, isGuestUser,
+      rendererOptions, page, isMobile, isGuestUser,
     } = this.props;
-    const { markdown, revisionId } = pageContainer.state;
+    const { path } = page;
+    const { _id: revisionId, body: markdown } = page.revision;
+
+    // const DrawioModal = dynamic(() => import('./PageEditor/DrawioModal'), { ssr: false });
+    // const GridEditModal = dynamic(() => import('./PageEditor/GridEditModal'), { ssr: false });
+    // const HandsontableModal = dynamic(() => import('./PageEditor/HandsontableModal'), { ssr: false });
+    // const LinkEditModal = dynamic(() => import('./PageEditor/LinkEditModal'), { ssr: false });
 
     return (
       <div className={`mb-5 ${isMobile ? 'page-mobile' : ''}`}>
 
         { revisionId != null && (
-          <RevisionRenderer growiRenderer={this.growiRenderer} markdown={markdown} pagePath={pagePath} />
+          <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown} pagePath={path} />
         )}
 
         { !isGuestUser && (
           <>
-            <GridEditModal ref={this.gridEditModal} />
-            <LinkEditModal ref={this.LinkEditModal} />
-            <HandsontableModal ref={this.handsontableModal} onSave={this.saveHandlerForHandsontableModal} />
-            <DrawioModal ref={this.drawioModal} onSave={this.saveHandlerForDrawioModal} />
+            {/* <GridEditModal ref={this.gridEditModal} /> */}
+            {/* <LinkEditModal ref={this.LinkEditModal} /> */}
+            {/* <HandsontableModal ref={this.handsontableModal} onSave={this.saveHandlerForHandsontableModal} /> */}
+            {/* <DrawioModal ref={this.drawioModal} onSave={this.saveHandlerForDrawioModal} /> */}
           </>
         )}
       </div>
@@ -166,89 +169,90 @@ class Page extends React.Component {
 
 }
 
-Page.propTypes = {
-  // TODO: remove this when omitting unstated is completed
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
-  editorContainer: PropTypes.instanceOf(EditorContainer).isRequired,
+PageSubstance.propTypes = {
+  rendererOptions: PropTypes.object.isRequired,
 
-  pagePath: PropTypes.string.isRequired,
+  page: PropTypes.any.isRequired,
   pageTags:  PropTypes.arrayOf(PropTypes.string),
   editorMode: PropTypes.string.isRequired,
   isGuestUser: PropTypes.bool.isRequired,
   isMobile: PropTypes.bool,
   isSlackEnabled: PropTypes.bool.isRequired,
   slackChannels: PropTypes.string.isRequired,
-  grant: PropTypes.number.isRequired,
-  grantGroupId: PropTypes.string,
-  grantGroupName: PropTypes.string,
 };
 
-const PageWrapper = (props) => {
-  const { data: currentPagePath } = useCurrentPagePath();
+export const Page = (props) => {
+  const { data: currentPage } = useSWRxCurrentPage();
   const { data: editorMode } = useEditorMode();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isMobile } = useIsMobile();
-  const { data: slackChannelsData } = useSWRxSlackChannels(currentPagePath);
+  const { data: slackChannelsData } = useSWRxSlackChannels(currentPage?.path);
   const { data: isSlackEnabled } = useIsSlackEnabled();
   const { data: pageTags } = usePageTagsForEditors();
-  const { data: grant } = useSelectedGrant();
-  const { data: grantGroupId } = useSelectedGrantGroupId();
-  const { data: grantGroupName } = useSelectedGrantGroupName();
+  const { data: rendererOptions } = useViewOptions();
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
+  const { data: isBlinkedAtBoot, mutate: mutateBlinkedAtBoot } = useIsBlinkedHeaderAtBoot();
 
   const pageRef = useRef(null);
 
-  // set handler to open DrawioModal
-  useEffect(() => {
-    const handler = (beginLineNumber, endLineNumber) => {
-      if (pageRef?.current != null) {
-        pageRef.current.launchDrawioModal(beginLineNumber, endLineNumber);
-      }
-    };
-    window.globalEmitter.on('launchDrawioModal', handler);
-
-    return function cleanup() {
-      window.globalEmitter.removeListener('launchDrawioModal', handler);
-    };
-  }, []);
-
-  // set handler to open HandsontableModal
   useEffect(() => {
-    const handler = (beginLineNumber, endLineNumber) => {
-      if (pageRef?.current != null) {
-        pageRef.current.launchHandsontableModal(beginLineNumber, endLineNumber);
-      }
-    };
-    window.globalEmitter.on('launchHandsontableModal', handler);
-
-    return function cleanup() {
-      window.globalEmitter.removeListener('launchHandsontableModal', handler);
-    };
-  }, []);
+    if (isBlinkedAtBoot) {
+      return;
+    }
 
-  if (currentPagePath == null || editorMode == null || isGuestUser == null) {
+    blinkSectionHeaderAtBoot();
+    mutateBlinkedAtBoot(true);
+  }, [mutateBlinkedAtBoot]);
+
+  // // set handler to open DrawioModal
+  // useEffect(() => {
+  //   const handler = (beginLineNumber, endLineNumber) => {
+  //     if (pageRef?.current != null) {
+  //       pageRef.current.launchDrawioModal(beginLineNumber, endLineNumber);
+  //     }
+  //   };
+  //   window.globalEmitter.on('launchDrawioModal', handler);
+
+  //   return function cleanup() {
+  //     window.globalEmitter.removeListener('launchDrawioModal', handler);
+  //   };
+  // }, []);
+
+  // // set handler to open HandsontableModal
+  // useEffect(() => {
+  //   const handler = (beginLineNumber, endLineNumber) => {
+  //     if (pageRef?.current != null) {
+  //       pageRef.current.launchHandsontableModal(beginLineNumber, endLineNumber);
+  //     }
+  //   };
+  //   window.globalEmitter.on('launchHandsontableModal', handler);
+
+  //   return function cleanup() {
+  //     window.globalEmitter.removeListener('launchHandsontableModal', handler);
+  //   };
+  // }, []);
+
+  if (currentPage == null || editorMode == null || isGuestUser == null || rendererOptions == null) {
+    logger.warn('Some of materials are missing.', {
+      currentPage: currentPage?._id, editorMode, isGuestUser, rendererOptions,
+    });
     return null;
   }
 
 
   return (
-    <Page
+    <PageSubstance
       {...props}
       ref={pageRef}
-      pagePath={currentPagePath}
+      rendererOptions={rendererOptions}
+      page={currentPage}
       editorMode={editorMode}
       isGuestUser={isGuestUser}
       isMobile={isMobile}
       isSlackEnabled={isSlackEnabled}
       pageTags={pageTags}
       slackChannels={slackChannelsData.toString()}
-      grant={grant}
-      grantGroupId={grantGroupId}
-      grantGroupName={grantGroupName}
       mutateIsEnabledUnsavedWarning={mutateIsEnabledUnsavedWarning}
     />
   );
 };
-
-export default withUnstatedContainers(PageWrapper, [AppContainer, PageContainer, EditorContainer]);

+ 7 - 5
packages/app/src/components/Page/DisplaySwitcher.tsx

@@ -8,7 +8,7 @@ import { TabContent, TabPane } from 'reactstrap';
 import { smoothScrollIntoView } from '~/client/util/smooth-scroll';
 import { isPopulated } from '~/interfaces/common';
 import {
-  useCurrentPagePath, useIsSharedUser, useIsEditable, useIsUserPage, usePageUser, useShareLinkId, useIsNotFound,
+  useCurrentPagePath, useIsSharedUser, useIsEditable, useIsUserPage, usePageUser, useShareLinkId, useIsNotFound, useIsNotCreatable,
 } from '~/stores/context';
 import { useDescendantsPageListModal } from '~/stores/modal';
 import { useSWRxCurrentPage } from '~/stores/page';
@@ -16,8 +16,9 @@ import { EditorMode, useEditorMode } from '~/stores/ui';
 
 import CountBadge from '../Common/CountBadge';
 import PageListIcon from '../Icons/PageListIcon';
+import { NotCreatablePage } from '../NotCreatablePage';
 import NotFoundPage from '../NotFoundPage';
-// import Page from '../Page';
+import { Page } from '../Page';
 // import PageEditor from '../PageEditor';
 // import PageEditorByHackmd from '../PageEditorByHackmd';
 import TableOfContents from '../TableOfContents';
@@ -46,6 +47,7 @@ const DisplaySwitcher = (): JSX.Element => {
   const { data: isEditable } = useIsEditable();
   const { data: pageUser } = usePageUser();
   const { data: isNotFound } = useIsNotFound();
+  const { data: isNotCreatable } = useIsNotCreatable();
   const { data: currentPage } = useSWRxCurrentPage(shareLinkId ?? undefined);
 
   const { data: editorMode } = useEditorMode();
@@ -114,9 +116,9 @@ const DisplaySwitcher = (): JSX.Element => {
 
             <div className="flex-grow-1 flex-basis-0 mw-0">
               { isUserPage && <UserInfo pageUser={pageUser} />}
-              {/* { !isNotFound && <Page /> } */}
-              { !isNotFound && revision != null && isPopulated(revision) && revision.body }
-              { isNotFound && <NotFoundPage /> }
+              { !isNotFound && <Page /> }
+              { isNotFound && !isNotCreatable && <NotFoundPage /> }
+              { isNotFound && isNotCreatable && <NotCreatablePage /> }
             </div>
 
           </div>

+ 0 - 1
packages/app/src/components/Page/RevisionBody.jsx

@@ -73,7 +73,6 @@ export default class RevisionBody extends React.PureComponent {
 
 RevisionBody.propTypes = {
   html: PropTypes.string,
-  isMathJaxEnabled: PropTypes.bool,
   renderMathJaxOnInit: PropTypes.bool,
   renderMathJaxInRealtime: PropTypes.bool,
   additionalClassName: PropTypes.string,

+ 4 - 4
packages/app/src/components/Page/RevisionLoader.jsx

@@ -1,11 +1,11 @@
 import React from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 import { Waypoint } from 'react-waypoint';
 
-import GrowiRenderer from '~/client/util/GrowiRenderer';
 import { apiv3Get } from '~/client/util/apiv3-client';
+import { RendererOptions } from '~/services/renderer/renderer';
 import loggerFactory from '~/utils/logger';
 
 import RevisionRenderer from './RevisionRenderer';
@@ -109,7 +109,7 @@ class RevisionLoader extends React.Component {
 
     return (
       <RevisionRenderer
-        growiRenderer={this.props.growiRenderer}
+        rendererOptions={this.props.rendererOptions}
         markdown={markdown}
         pagePath={this.props.pagePath}
         highlightKeywords={this.props.highlightKeywords}
@@ -123,7 +123,7 @@ class RevisionLoader extends React.Component {
 RevisionLoader.propTypes = {
   t: PropTypes.func.isRequired,
 
-  growiRenderer: PropTypes.instanceOf(GrowiRenderer).isRequired,
+  rendererOptions: PropTypes.instanceOf(RendererOptions).isRequired,
   pageId: PropTypes.string.isRequired,
   pagePath: PropTypes.string.isRequired,
   revisionId: PropTypes.string.isRequired,

+ 0 - 205
packages/app/src/components/Page/RevisionRenderer.jsx

@@ -1,205 +0,0 @@
-import React from 'react';
-
-import PropTypes from 'prop-types';
-
-import AppContainer from '~/client/services/AppContainer';
-import GrowiRenderer from '~/client/util/GrowiRenderer';
-import { blinkElem } from '~/client/util/blink-section-header';
-import { addSmoothScrollEvent } from '~/client/util/smooth-scroll';
-import { useEditorSettings } from '~/stores/editor';
-
-import { withUnstatedContainers } from '../UnstatedUtils';
-
-import RevisionBody from './RevisionBody';
-
-import { loggerFactory } from '^/../codemirror-textlint/src/utils/logger';
-
-const logger = loggerFactory('components:Page:RevisionRenderer');
-
-class LegacyRevisionRenderer extends React.PureComponent {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      html: '',
-    };
-
-    this.renderHtml = this.renderHtml.bind(this);
-    this.getHighlightedBody = this.getHighlightedBody.bind(this);
-  }
-
-  initCurrentRenderingContext() {
-    this.currentRenderingContext = {
-      markdown: this.props.markdown,
-      pagePath: this.props.pagePath,
-      renderDrawioInRealtime: this.props.editorSettings?.renderDrawioInRealtime,
-      currentPathname: decodeURIComponent(window.location.pathname),
-    };
-  }
-
-  componentDidMount() {
-    this.initCurrentRenderingContext();
-    this.renderHtml();
-  }
-
-  componentDidUpdate(prevProps) {
-    const { markdown: prevMarkdown, highlightKeywords: prevHighlightKeywords } = prevProps;
-    const { markdown, highlightKeywords } = this.props;
-
-    // render only when props.markdown is updated
-    if (markdown !== prevMarkdown || highlightKeywords !== prevHighlightKeywords) {
-      this.initCurrentRenderingContext();
-      this.renderHtml();
-      return;
-    }
-
-    const HeaderLink = document.getElementsByClassName('revision-head-link');
-    const HeaderLinkArray = Array.from(HeaderLink);
-    addSmoothScrollEvent(HeaderLinkArray, blinkElem);
-
-    const { interceptorManager } = window;
-
-    interceptorManager.process('postRenderHtml', this.currentRenderingContext);
-  }
-
-  /**
-   * transplanted from legacy code -- Yuki Takei
-   * @param {string} body html strings
-   * @param {string} keywords
-   */
-  getHighlightedBody(body, keywords) {
-    const normalizedKeywordsArray = [];
-    // !!TODO!!: add test code refs: https://redmine.weseek.co.jp/issues/86841
-    // Separate keywords
-    // - Surrounded by double quotation
-    // - Split by both full-width and half-width spaces
-    // [...keywords.match(/"[^"]+"|[^\u{20}\u{3000}]+/ug)].forEach((keyword, i) => {
-    keywords.forEach((keyword, i) => {
-      if (keyword === '') {
-        return;
-      }
-      const k = keyword
-        .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // escape regex operators
-        .replace(/(^"|"$)/g, ''); // for phrase (quoted) keyword
-      normalizedKeywordsArray.push(k);
-    });
-
-    const normalizedKeywords = `(${normalizedKeywordsArray.join('|')})`;
-    const keywordRegxp = new RegExp(`${normalizedKeywords}(?!(.*?"))`, 'ig'); // prior https://regex101.com/r/oX7dq5/1
-    let keywordRegexp2 = keywordRegxp;
-
-    // for non-chrome browsers compatibility
-    try {
-      // eslint-disable-next-line regex/invalid
-      keywordRegexp2 = new RegExp(`(?<!<)${normalizedKeywords}(?!(.*?("|>)))`, 'ig'); // inferior (this doesn't work well when html tags exist a lot) https://regex101.com/r/Dfi61F/1
-    }
-    catch (err) {
-      logger.debug('Failed to initialize regex:', err);
-    }
-
-    const highlighter = (str) => { return str.replace(keywordRegxp, '<em class="highlighted-keyword">$&</em>') }; // prior
-    const highlighter2 = (str) => { return str.replace(keywordRegexp2, '<em class="highlighted-keyword">$&</em>') }; // inferior
-
-    const insideTagRegex = /<[^<>]*>/g;
-    const betweenTagRegex = />([^<>]*)</g; // use (group) to ignore >< around
-
-    const insideTagStrs = body.match(insideTagRegex);
-    const betweenTagMatches = Array.from(body.matchAll(betweenTagRegex));
-
-    let returnBody = body;
-    const isSafeHtml = insideTagStrs.length === betweenTagMatches.length + 1; // to check whether is safe to join
-    if (isSafeHtml) {
-      // highlight
-      const betweenTagStrs = betweenTagMatches.map(match => highlighter(match[1])); // get only grouped part (exclude >< around)
-
-      const arr = [];
-      insideTagStrs.forEach((str, i) => {
-        arr.push(str);
-        arr.push(betweenTagStrs[i]);
-      });
-      returnBody = arr.join('');
-    }
-    else {
-      // inferior highlighter
-      returnBody = highlighter2(body);
-    }
-
-    return returnBody;
-  }
-
-  async renderHtml() {
-    const {
-      appContainer, growiRenderer,
-      highlightKeywords,
-    } = this.props;
-
-    const { interceptorManager } = window;
-    const context = this.currentRenderingContext;
-
-    await interceptorManager.process('preRender', context);
-    await interceptorManager.process('prePreProcess', context);
-    context.markdown = growiRenderer.preProcess(context.markdown, context);
-    await interceptorManager.process('postPreProcess', context);
-    context.parsedHTML = growiRenderer.process(context.markdown, context);
-    await interceptorManager.process('prePostProcess', context);
-    context.parsedHTML = growiRenderer.postProcess(context.parsedHTML, context);
-
-    const isMarkdownEmpty = context.markdown.trim().length === 0;
-    if (highlightKeywords != null && highlightKeywords.length > 0 && !isMarkdownEmpty) {
-      context.parsedHTML = this.getHighlightedBody(context.parsedHTML, highlightKeywords);
-    }
-    await interceptorManager.process('postPostProcess', context);
-    await interceptorManager.process('preRenderHtml', context);
-
-    this.setState({ html: context.parsedHTML });
-  }
-
-  render() {
-    const config = this.props.appContainer.getConfig();
-    const isMathJaxEnabled = !!config.env.MATHJAX;
-
-    return (
-      <RevisionBody
-        html={this.state.html}
-        isMathJaxEnabled={isMathJaxEnabled}
-        additionalClassName={this.props.additionalClassName}
-        renderMathJaxOnInit
-      />
-    );
-  }
-
-}
-
-LegacyRevisionRenderer.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  growiRenderer: PropTypes.instanceOf(GrowiRenderer).isRequired,
-  markdown: PropTypes.string.isRequired,
-  pagePath: PropTypes.string.isRequired,
-  highlightKeywords: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
-  additionalClassName: PropTypes.string,
-  editorSettings: PropTypes.any,
-};
-
-/**
- * Wrapper component for using unstated
- */
-const LegacyRevisionRendererWrapper = withUnstatedContainers(LegacyRevisionRenderer, [AppContainer]);
-
-
-// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-const RevisionRenderer = (props) => {
-  const { data: editorSettings } = useEditorSettings();
-
-  return <LegacyRevisionRendererWrapper {...props} editorSettings={editorSettings} />;
-};
-
-RevisionRenderer.propTypes = {
-  growiRenderer: PropTypes.instanceOf(GrowiRenderer).isRequired,
-  markdown: PropTypes.string.isRequired,
-  pagePath: PropTypes.string.isRequired,
-  highlightKeywords: PropTypes.arrayOf(PropTypes.string),
-  additionalClassName: PropTypes.string,
-};
-
-export default RevisionRenderer;

+ 176 - 0
packages/app/src/components/Page/RevisionRenderer.tsx

@@ -0,0 +1,176 @@
+import React from 'react';
+
+import ReactMarkdown from 'react-markdown';
+
+import { blinkElem } from '~/client/util/blink-section-header';
+import { addSmoothScrollEvent } from '~/client/util/smooth-scroll';
+import { CustomWindow } from '~/interfaces/global';
+import { RendererOptions } from '~/services/renderer/renderer';
+import { useCurrentPathname, useInterceptorManager } from '~/stores/context';
+import { useEditorSettings } from '~/stores/editor';
+import { useViewOptions } from '~/stores/renderer';
+import loggerFactory from '~/utils/logger';
+
+// import RevisionBody from './RevisionBody';
+
+
+const logger = loggerFactory('components:Page:RevisionRenderer');
+
+
+// function getHighlightedBody(body: string, _keywords: string | string[]): string {
+//   const normalizedKeywordsArray: string[] = [];
+
+//   const keywords = (typeof _keywords === 'string') ? [_keywords] : _keywords;
+
+//   if (keywords.length === 0) {
+//     return body;
+//   }
+
+//   // !!TODO!!: add test code refs: https://redmine.weseek.co.jp/issues/86841
+//   // Separate keywords
+//   // - Surrounded by double quotation
+//   // - Split by both full-width and half-width spaces
+//   // [...keywords.match(/"[^"]+"|[^\u{20}\u{3000}]+/ug)].forEach((keyword, i) => {
+//   keywords.forEach((keyword, i) => {
+//     if (keyword === '') {
+//       return;
+//     }
+//     const k = keyword
+//       .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // escape regex operators
+//       .replace(/(^"|"$)/g, ''); // for phrase (quoted) keyword
+//     normalizedKeywordsArray.push(k);
+//   });
+
+//   const normalizedKeywords = `(${normalizedKeywordsArray.join('|')})`;
+//   const keywordRegxp = new RegExp(`${normalizedKeywords}(?!(.*?"))`, 'ig'); // prior https://regex101.com/r/oX7dq5/1
+//   let keywordRegexp2 = keywordRegxp;
+
+//   // for non-chrome browsers compatibility
+//   try {
+//     // eslint-disable-next-line regex/invalid
+//     keywordRegexp2 = new RegExp(`(?<!<)${normalizedKeywords}(?!(.*?("|>)))`, 'ig'); // inferior (this doesn't work well when html tags exist a lot) https://regex101.com/r/Dfi61F/1
+//   }
+//   catch (err) {
+//     logger.debug('Failed to initialize regex:', err);
+//   }
+
+//   const highlighter = (str) => { return str.replace(keywordRegxp, '<em class="highlighted-keyword">$&</em>') }; // prior
+//   const highlighter2 = (str) => { return str.replace(keywordRegexp2, '<em class="highlighted-keyword">$&</em>') }; // inferior
+
+//   const insideTagRegex = /<[^<>]*>/g;
+//   const betweenTagRegex = />([^<>]*)</g; // use (group) to ignore >< around
+
+//   const insideTagStrs = body.match(insideTagRegex);
+//   const betweenTagMatches = Array.from(body.matchAll(betweenTagRegex));
+
+//   let returnBody = body;
+//   const isSafeHtml = insideTagStrs?.length === betweenTagMatches.length + 1; // to check whether is safe to join
+//   if (isSafeHtml) {
+//     // highlight
+//     const betweenTagStrs: string[] = betweenTagMatches.map(match => highlighter(match[1])); // get only grouped part (exclude >< around)
+
+//     const arr: string[] = [];
+//     insideTagStrs.forEach((str, i) => {
+//       arr.push(str);
+//       arr.push(betweenTagStrs[i]);
+//     });
+//     returnBody = arr.join('');
+//   }
+//   else {
+//     // inferior highlighter
+//     returnBody = highlighter2(body);
+//   }
+
+//   return returnBody;
+// }
+
+
+type Props = {
+  rendererOptions: RendererOptions,
+  markdown: string,
+  pagePath: string,
+  highlightKeywords?: string | string[],
+  additionalClassName?: string,
+}
+
+const RevisionRenderer = (props: Props): JSX.Element => {
+
+  const {
+    rendererOptions, markdown, pagePath, highlightKeywords, additionalClassName,
+  } = props;
+
+  return (
+    <ReactMarkdown {...rendererOptions} className={`wiki ${additionalClassName ?? ''}`}>
+      {markdown}
+    </ReactMarkdown>
+  );
+
+  // const [html, setHtml] = useState('');
+
+  // const { data: interceptorManager } = useInterceptorManager();
+  // const { data: editorSettings } = useEditorSettings();
+  // const { data: currentPathname } = useCurrentPathname();
+
+  // const currentRenderingContext = useMemo(() => {
+  //   return {
+  //     markdown,
+  //     parsedHTML: '',
+  //     pagePath,
+  //     renderDrawioInRealtime: editorSettings?.renderDrawioInRealtime,
+  //     currentPathname: decodeURIComponent(currentPathname ?? '/'),
+  //   };
+  // }, [editorSettings?.renderDrawioInRealtime, markdown, pagePath]);
+
+
+  // const renderHtml = useCallback(async() => {
+  //   if (interceptorManager == null) {
+  //     return;
+  //   }
+
+  //   const context = currentRenderingContext;
+
+  //   await interceptorManager.process('preRender', context);
+  //   await interceptorManager.process('prePreProcess', context);
+  //   context.markdown = growiRenderer.preProcess(context.markdown, context);
+  //   await interceptorManager.process('postPreProcess', context);
+  //   context.parsedHTML = growiRenderer.process(context.markdown, context);
+  //   await interceptorManager.process('prePostProcess', context);
+  //   context.parsedHTML = growiRenderer.postProcess(context.parsedHTML, context);
+
+  //   const isMarkdownEmpty = context.markdown.trim().length === 0;
+  //   if (highlightKeywords != null && !isMarkdownEmpty) {
+  //     context.parsedHTML = getHighlightedBody(context.parsedHTML, highlightKeywords);
+  //   }
+  //   await interceptorManager.process('postPostProcess', context);
+  //   await interceptorManager.process('preRenderHtml', context);
+
+  //   setHtml(context.parsedHTML);
+  // }, [currentRenderingContext, growiRenderer, highlightKeywords, interceptorManager]);
+
+  // useEffect(() => {
+  //   if (interceptorManager == null) {
+  //     return;
+  //   }
+
+  //   renderHtml()
+  //     .then(() => {
+  //       // const HeaderLink = document.getElementsByClassName('revision-head-link');
+  //       // const HeaderLinkArray = Array.from(HeaderLink);
+  //       // addSmoothScrollEvent(HeaderLinkArray as HTMLAnchorElement[], blinkElem);
+
+  //       // interceptorManager.process('postRenderHtml', currentRenderingContext);
+  //     });
+
+  // }, [currentRenderingContext, interceptorManager, renderHtml]);
+
+  // return (
+  //   <RevisionBody
+  //     html={html}
+  //     additionalClassName={props.additionalClassName}
+  //     renderMathJaxOnInit
+  //   />
+  // );
+
+};
+
+export default RevisionRenderer;

+ 12 - 10
packages/app/src/components/PageAlert/PageGrantAlert.tsx

@@ -1,7 +1,9 @@
 import React from 'react';
+
+import { useTranslation } from 'react-i18next';
+
 import { useSWRxCurrentPage } from '~/stores/page';
 import { useXss } from '~/stores/xss';
-import { useTranslation } from 'react-i18next';
 
 
 export const PageGrantAlert = (): JSX.Element => {
@@ -9,32 +11,32 @@ export const PageGrantAlert = (): JSX.Element => {
   const { data: pageData } = useSWRxCurrentPage();
   const { data: xss } = useXss();
 
-  if ( pageData == null || pageData.grant == null || pageData.grant == 1 || xss == null) {
-    return <></>
+  if (pageData == null || pageData.grant == null || pageData.grant === 1 || xss == null) {
+    return <></>;
   }
 
   const renderAlertContent = () => {
     const getGrantLabel = () => {
-      if (pageData.grant == 2) {
+      if (pageData.grant === 2) {
         return (
           <>
             <i className="icon-fw icon-link"></i><strong>{t('Anyone with the link')} only</strong>
           </>
-        )
+        );
       }
-      if (pageData.grant == 4) {
+      if (pageData.grant === 4) {
         return (
           <>
             <i className="icon-fw icon-lock"></i><strong>{t('Only me')} only</strong>
           </>
-        )
+        );
       }
-      if (pageData.grant == 5) {
+      if (pageData.grant === 5) {
         return (
           <>
             <i className="icon-fw icon-organization"></i><strong>{xss.process(pageData.grantedGroup.name)} only</strong>
           </>
-        )
+        );
       }
     };
     return (
@@ -50,4 +52,4 @@ export const PageGrantAlert = (): JSX.Element => {
       {renderAlertContent()}
     </p>
   );
-}
+};

+ 12 - 11
packages/app/src/components/PageAlert/PageStaleAlert.tsx

@@ -1,9 +1,10 @@
-import { useIsEnabledStaleNotification } from '../../stores/context'
-import { useSWRxCurrentPage, useSWRxPageInfo } from '../../stores/page'
 import { useTranslation } from 'react-i18next';
 
+import { useIsEnabledStaleNotification } from '../../stores/context';
+import { useSWRxCurrentPage, useSWRxPageInfo } from '../../stores/page';
+
 export const PageStaleAlert = ():JSX.Element => {
-  const { t } = useTranslation()
+  const { t } = useTranslation();
   const { data: isEnabledStaleNotification } = useIsEnabledStaleNotification();
 
   // Todo: determine if it should fetch or not like useSWRxPageInfo below after https://redmine.weseek.co.jp/issues/96788
@@ -13,23 +14,23 @@ export const PageStaleAlert = ():JSX.Element => {
   const contentAge = pageInfo?.contentAge;
 
   if (!isEnabledStaleNotification) {
-    return <></>
+    return <></>;
   }
 
-  if( pageInfo == null || contentAge == null || contentAge === 0) {
-    return <></>
+  if (pageInfo == null || contentAge == null || contentAge === 0) {
+    return <></>;
   }
 
   let alertClass;
   switch (contentAge) {
     case 1:
-      alertClass = "alert-info";
+      alertClass = 'alert-info';
       break;
     case 2:
-      alertClass = "alert-warning";
+      alertClass = 'alert-warning';
       break;
     default:
-      alertClass = "alert-danger";
+      alertClass = 'alert-danger';
   }
 
   return (
@@ -37,5 +38,5 @@ export const PageStaleAlert = ():JSX.Element => {
       <i className="icon-fw icon-hourglass"></i>
       <strong>{ t('page_page.notice.stale', { count: pageInfo.contentAge }) }</strong>
     </div>
-  )
-}
+  );
+};

+ 9 - 3
packages/app/src/components/PageComment.tsx

@@ -8,6 +8,7 @@ import { Button } from 'reactstrap';
 import AppContainer from '~/client/services/AppContainer';
 import { toastError } from '~/client/util/apiNotification';
 import { apiPost } from '~/client/util/apiv1-client';
+import { useCommentPreviewOptions } from '~/stores/renderer';
 
 import { ICommentHasId, ICommentHasIdList } from '../interfaces/comment';
 import { useSWRxPageComment } from '../stores/comment';
@@ -35,6 +36,7 @@ const PageComment:FC<Props> = memo((props:Props):JSX.Element => {
   } = props;
 
   const { data: comments, mutate } = useSWRxPageComment(pageId);
+  const { data: rendererOptions } = useCommentPreviewOptions();
 
   const [commentToBeDeleted, setCommentToBeDeleted] = useState<ICommentHasId | null>(null);
   const [isDeleteConfirmModalShown, setIsDeleteConfirmModalShown] = useState<boolean>(false);
@@ -110,7 +112,7 @@ const PageComment:FC<Props> = memo((props:Props):JSX.Element => {
 
   const generateCommentInnerElement = (comment: ICommentHasId) => (
     <Comment
-      growiRenderer={appContainer.getRenderer('comment')}
+      rendererOptions={rendererOptions}
       deleteBtnClicked={onClickDeleteButton}
       comment={comment}
       onComment={mutate}
@@ -122,7 +124,7 @@ const PageComment:FC<Props> = memo((props:Props):JSX.Element => {
     <ReplayComments
       replyList={replyComments}
       deleteBtnClicked={onClickDeleteButton}
-      growiRenderer={appContainer.getRenderer('comment')}
+      rendererOptions={rendererOptions}
       isReadOnly={isReadOnly}
     />
   );
@@ -142,6 +144,10 @@ const PageComment:FC<Props> = memo((props:Props):JSX.Element => {
     return <></>;
   }
 
+  if (rendererOptions == null) {
+    return <></>;
+  }
+
   let commentTitleClasses = 'border-bottom py-3 mb-3';
   commentTitleClasses = titleAlign != null ? `${commentTitleClasses} text-${titleAlign}` : `${commentTitleClasses} text-center`;
 
@@ -185,7 +191,7 @@ const PageComment:FC<Props> = memo((props:Props):JSX.Element => {
                     {/* display reply editor */}
                     {(!isReadOnly && showEditorIds.has(comment._id)) && (
                       <CommentEditor
-                        growiRenderer={appContainer.getRenderer('comment')}
+                        rendererOptions={rendererOptions}
                         replyTo={comment._id}
                         onCancelButtonClicked={() => {
                           removeShowEditorId(comment._id);

+ 7 - 6
packages/app/src/components/PageComment/Comment.jsx

@@ -19,6 +19,7 @@ import Username from '../User/Username';
 import CommentControl from './CommentControl';
 import CommentEditor from './CommentEditor';
 
+import { RendererOptions } from '~/services/renderer/renderer';
 
 /**
  *
@@ -132,17 +133,17 @@ class Comment extends React.PureComponent {
 
   async renderHtml() {
 
-    const { growiRenderer, appContainer } = this.props;
+    const { rendererOptions, appContainer } = this.props;
     const { interceptorManager } = window;
     const context = this.currentRenderingContext;
 
     await interceptorManager.process('preRenderComment', context);
     await interceptorManager.process('prePreProcess', context);
-    context.markdown = await growiRenderer.preProcess(context.markdown, context);
+    context.markdown = await rendererOptions.preProcess(context.markdown, context);
     await interceptorManager.process('postPreProcess', context);
-    context.parsedHTML = await growiRenderer.process(context.markdown, context);
+    context.parsedHTML = await rendererOptions.process(context.markdown, context);
     await interceptorManager.process('prePostProcess', context);
-    context.parsedHTML = await growiRenderer.postProcess(context.parsedHTML, context);
+    context.parsedHTML = await rendererOptions.postProcess(context.parsedHTML, context);
     await interceptorManager.process('postPostProcess', context);
     await interceptorManager.process('preRenderCommentHtml', context);
     this.setState({ html: context.parsedHTML });
@@ -173,7 +174,7 @@ class Comment extends React.PureComponent {
       <React.Fragment>
         {(this.state.isReEdit && !isReadOnly) ? (
           <CommentEditor
-            growiRenderer={this.props.growiRenderer}
+            rendererOptions={this.props.rendererOptions}
             currentCommentId={commentId}
             commentBody={comment.comment}
             replyTo={undefined}
@@ -236,7 +237,7 @@ Comment.propTypes = {
 
   comment: PropTypes.object.isRequired,
   isReadOnly: PropTypes.bool.isRequired,
-  growiRenderer: PropTypes.object.isRequired,
+  rendererOptions: PropTypes.instanceOf(RendererOptions).isRequired,
   deleteBtnClicked: PropTypes.func.isRequired,
   currentUser: PropTypes.object,
   onComment: PropTypes.func,

+ 7 - 7
packages/app/src/components/PageComment/CommentEditor.tsx

@@ -12,10 +12,10 @@ import * as toastr from 'toastr';
 import AppContainer from '~/client/services/AppContainer';
 import EditorContainer from '~/client/services/EditorContainer';
 import PageContainer from '~/client/services/PageContainer';
-import GrowiRenderer from '~/client/util/GrowiRenderer';
 import { apiPostForm } from '~/client/util/apiv1-client';
 import { CustomWindow } from '~/interfaces/global';
 import { IInterceptorManager } from '~/interfaces/interceptor-manager';
+import { RendererOptions } from '~/services/renderer/renderer';
 import { useSWRxPageComment } from '~/stores/comment';
 import {
   useCurrentPagePath, useCurrentPageId, useCurrentUser, useRevisionId,
@@ -49,7 +49,7 @@ const navTabMapping = {
 type PropsType = {
   appContainer: AppContainer,
 
-  growiRenderer: GrowiRenderer,
+  rendererOptions: RendererOptions,
   isForNewComment?: boolean,
   replyTo?: string,
   currentCommentId?: string,
@@ -68,7 +68,7 @@ type EditorRef = {
 const CommentEditor = (props: PropsType): JSX.Element => {
 
   const {
-    appContainer, growiRenderer, isForNewComment, replyTo,
+    appContainer, rendererOptions, isForNewComment, replyTo,
     currentCommentId, commentBody, commentCreator, onCancelButtonClicked, onCommentButtonClicked,
   } = props;
   const { data: currentUser } = useCurrentUser();
@@ -104,16 +104,16 @@ const CommentEditor = (props: PropsType): JSX.Element => {
     interceptorManager.process('preRenderCommnetPreview', context)
       .then(() => { return interceptorManager.process('prePreProcess', context) })
       .then(() => {
-        context.markdown = growiRenderer.preProcess(context.markdown, context);
+        context.markdown = rendererOptions.preProcess(context.markdown, context);
       })
       .then(() => { return interceptorManager.process('postPreProcess', context) })
       .then(() => {
-        const parsedHTML = growiRenderer.process(context.markdown, context);
+        const parsedHTML = rendererOptions.process(context.markdown, context);
         context.parsedHTML = parsedHTML;
       })
       .then(() => { return interceptorManager.process('prePostProcess', context) })
       .then(() => {
-        context.parsedHTML = growiRenderer.postProcess(context.parsedHTML, context);
+        context.parsedHTML = rendererOptions.postProcess(context.parsedHTML, context);
       })
       .then(() => { return interceptorManager.process('postPostProcess', context) })
       .then(() => { return interceptorManager.process('preRenderCommentPreviewHtml', context) })
@@ -122,7 +122,7 @@ const CommentEditor = (props: PropsType): JSX.Element => {
       })
       // process interceptors for post rendering
       .then(() => { return interceptorManager.process('postRenderCommentPreviewHtml', context) });
-  }, [growiRenderer]);
+  }, [rendererOptions]);
 
   const handleSelect = useCallback((activeTab: string) => {
     setActiveTab(activeTab);

+ 7 - 2
packages/app/src/components/PageComment/CommentEditorLazyRenderer.tsx

@@ -5,6 +5,7 @@ import { useSWRxPageComment } from '../../stores/comment';
 import AppContainer from '~/client/services/AppContainer';
 
 import CommentEditor from './CommentEditor';
+import { useCommentPreviewOptions } from '~/stores/renderer';
 
 type Props = {
   appContainer: AppContainer,
@@ -15,14 +16,18 @@ const CommentEditorLazyRenderer:FC<Props> = (props:Props):JSX.Element => {
 
   const { pageId } = props;
   const { mutate } = useSWRxPageComment(pageId);
+  const { data: rendererOptions } = useCommentPreviewOptions();
+
+  if (rendererOptions == null) {
+    return <></>;
+  }
 
   const { appContainer } = props;
-  const growiRenderer = appContainer.getRenderer('comment');
 
   return (
     <CommentEditor
       appContainer={appContainer}
-      growiRenderer={growiRenderer}
+      rendererOptions={rendererOptions}
       replyTo={undefined}
       onCommentButtonClicked={mutate}
       isForNewComment

+ 4 - 2
packages/app/src/components/PageComment/ReplayComments.jsx

@@ -10,6 +10,8 @@ import Comment from './Comment';
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 
+import { RendererOptions } from '~/services/renderer/renderer';
+
 class ReplayComments extends React.PureComponent {
 
   constructor() {
@@ -32,7 +34,7 @@ class ReplayComments extends React.PureComponent {
         <Comment
           comment={reply}
           deleteBtnClicked={this.props.deleteBtnClicked}
-          growiRenderer={this.props.growiRenderer}
+          rendererOptions={this.props.rendererOptions}
           isReadOnly={this.props.isReadOnly}
         />
       </div>
@@ -107,7 +109,7 @@ ReplayComments.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
 
-  growiRenderer: PropTypes.object.isRequired,
+  rendererOptions: PropTypes.instanceOf(RendererOptions).isRequired,
   deleteBtnClicked: PropTypes.func.isRequired,
   isReadOnly: PropTypes.bool.isRequired,
   replyList: PropTypes.array,

+ 9 - 1
packages/app/src/components/PageEditor.tsx

@@ -20,6 +20,7 @@ import {
   useCurrentIndentSize, useSWRxSlackChannels, useIsSlackEnabled, useIsTextlintEnabled, usePageTagsForEditors,
   useIsEnabledUnsavedWarning,
 } from '~/stores/editor';
+import { usePreviewOptions } from '~/stores/renderer';
 import {
   EditorMode,
   useEditorMode, useIsMobile, useSelectedGrant, useSelectedGrantGroupId, useSelectedGrantGroupName,
@@ -99,6 +100,8 @@ const PageEditor = (props: Props): JSX.Element => {
   const { data: indentSize, mutate: mutateCurrentIndentSize } = useCurrentIndentSize();
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
 
+  const { data: rendererOptions } = usePreviewOptions();
+
   // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
   const [markdown, setMarkdown] = useState<string>(pageContainer.state.markdown!);
 
@@ -389,6 +392,10 @@ const PageEditor = (props: Props): JSX.Element => {
     return <></>;
   }
 
+  if (rendererOptions == null) {
+    return <></>;
+  }
+
   const config = props.appContainer.getConfig();
   const isUploadable = config.upload.image || config.upload.file;
   const isUploadableFile = config.upload.file;
@@ -425,8 +432,9 @@ const PageEditor = (props: Props): JSX.Element => {
       <div className="d-none d-lg-block page-editor-preview-container flex-grow-1 flex-basis-0 mw-0">
         <Preview
           markdown={markdown}
+          rendererOptions={rendererOptions}
           ref={previewRef}
-          isMathJaxEnabled={isMathJaxEnabled}
+          // isMathJaxEnabled={isMathJaxEnabled}
           renderMathJaxOnInit={false}
           onScroll={offset => scrollEditorByPreviewScrollWithThrottle(offset)}
         />

+ 8 - 9
packages/app/src/components/PageEditor/Preview.tsx

@@ -5,6 +5,7 @@ import React, {
 
 import AppContainer from '~/client/services/AppContainer';
 import InterceptorManager from '~/services/interceptor-manager';
+import { RendererOptions } from '~/services/renderer/renderer';
 import { useEditorSettings } from '~/stores/editor';
 
 import RevisionBody from '../Page/RevisionBody';
@@ -15,9 +16,9 @@ declare const interceptorManager: InterceptorManager;
 
 
 type Props = {
+  rendererOptions: RendererOptions,
   markdown?: string,
   pagePath?: string,
-  isMathJaxEnabled?: boolean,
   renderMathJaxOnInit?: boolean,
   onScroll?: (scrollTop: number) => void,
 }
@@ -27,7 +28,7 @@ type UnstatedProps = Props & { appContainer: AppContainer };
 const Preview = React.forwardRef((props: UnstatedProps, ref: RefObject<HTMLDivElement>): JSX.Element => {
 
   const {
-    appContainer,
+    rendererOptions,
     markdown, pagePath,
   } = props;
 
@@ -35,8 +36,6 @@ const Preview = React.forwardRef((props: UnstatedProps, ref: RefObject<HTMLDivEl
 
   const { data: editorSettings } = useEditorSettings();
 
-  const growiRenderer = appContainer.getRenderer('editor');
-
   const context = useMemo(() => {
     return {
       markdown,
@@ -51,17 +50,17 @@ const Preview = React.forwardRef((props: UnstatedProps, ref: RefObject<HTMLDivEl
     if (interceptorManager != null) {
       await interceptorManager.process('preRenderPreview', context);
       await interceptorManager.process('prePreProcess', context);
-      context.markdown = growiRenderer.preProcess(context.markdown, context);
+      context.markdown = rendererOptions.preProcess(context.markdown, context);
       await interceptorManager.process('postPreProcess', context);
-      context.parsedHTML = growiRenderer.process(context.markdown, context);
+      context.parsedHTML = rendererOptions.process(context.markdown, context);
       await interceptorManager.process('prePostProcess', context);
-      context.parsedHTML = growiRenderer.postProcess(context.parsedHTML, context);
+      context.parsedHTML = rendererOptions.postProcess(context.parsedHTML, context);
       await interceptorManager.process('postPostProcess', context);
       await interceptorManager.process('preRenderPreviewHtml', context);
     }
 
     setHtml(context.parsedHTML ?? '');
-  }, [interceptorManager, context, growiRenderer]);
+  }, [context, rendererOptions]);
 
   useEffect(() => {
     if (markdown == null) {
@@ -82,7 +81,7 @@ const Preview = React.forwardRef((props: UnstatedProps, ref: RefObject<HTMLDivEl
         parsedHTML: html,
       });
     }
-  }, [context, html, interceptorManager]);
+  }, [context, html]);
 
   return (
     <div

+ 13 - 4
packages/app/src/components/PageTimeline.jsx

@@ -6,6 +6,8 @@ import { useTranslation } from 'next-i18next';
 import AppContainer from '~/client/services/AppContainer';
 import PageContainer from '~/client/services/PageContainer';
 import { apiv3Get } from '~/client/util/apiv3-client';
+import { RendererOptions } from '~/services/renderer/renderer';
+import { useTimelineOptions } from '~/stores/renderer';
 
 import RevisionLoader from './Page/RevisionLoader';
 import PaginationWrapper from './PaginationWrapper';
@@ -48,9 +50,9 @@ class PageTimeline extends React.Component {
   }
 
   UNSAFE_componentWillMount() {
-    const { appContainer } = this.props;
+    const { rendererOptions } = this.props;
     // initialize GrowiRenderer
-    this.growiRenderer = appContainer.getRenderer('timeline');
+    this.rendererOptions = rendererOptions;
   }
 
   async componentDidMount() {
@@ -83,7 +85,7 @@ class PageTimeline extends React.Component {
                 <div className="card-body">
                   <RevisionLoader
                     lazy
-                    growiRenderer={this.growiRenderer}
+                    rendererOptions={this.rendererOptions}
                     pageId={page._id}
                     pagePath={page.path}
                     revisionId={page.revision}
@@ -110,13 +112,20 @@ class PageTimeline extends React.Component {
 PageTimeline.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  rendererOptions: PropTypes.instanceOf(RendererOptions).isRequired,
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
   pages: PropTypes.arrayOf(PropTypes.object),
 };
 
 const PageTimelineWrapperFC = (props) => {
   const { t } = useTranslation();
-  return <PageTimeline t={t} {...props} />;
+  const { data: rendererOptions } = useTimelineOptions();
+
+  if (rendererOptions == null) {
+    return <></>;
+  }
+
+  return <PageTimeline t={t} rendererOptions={rendererOptions} {...props} />;
 };
 
 /**

+ 49 - 0
packages/app/src/components/ReactMarkdownComponents/Header.tsx

@@ -0,0 +1,49 @@
+import { Element } from 'react-markdown/lib/rehype-filter';
+
+import { NextLink } from './NextLink';
+
+
+type EditLinkProps = {
+  line?: number,
+}
+
+/**
+ * Inner FC to display edit link icon
+ */
+const EditLink = (props: EditLinkProps): JSX.Element => {
+  const isDisabled = props.line == null;
+
+  return (
+    <span className="revision-head-edit-button">
+      <a href="#edit" aria-disabled={isDisabled} onClick={() => console.log(`TODO: Jump to the line '${props.line}'`)}>
+        <i className="icon-note"></i>
+      </a>
+    </span>
+  );
+};
+
+
+type HeaderProps = {
+  children: React.ReactNode,
+  node: Element,
+  level: number,
+  id?: string,
+}
+
+export const Header = (props: HeaderProps): JSX.Element => {
+  const {
+    node, id, children, level,
+  } = props;
+
+  const CustomTag = `h${level}` as keyof JSX.IntrinsicElements;
+
+  return (
+    <CustomTag id={id} className="revision-head">
+      {children}
+      <NextLink href={`#${id}`} className="revision-head-link">
+        <span className="icon-link"></span>
+      </NextLink>
+      <EditLink line={node.position?.start.line} />
+    </CustomTag>
+  );
+};

+ 46 - 0
packages/app/src/components/ReactMarkdownComponents/NextLink.tsx

@@ -0,0 +1,46 @@
+import Link, { LinkProps } from 'next/link';
+
+import { useSiteUrl } from '~/stores/context';
+
+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;
+};
+
+type Props = Omit<LinkProps, 'href'> & {
+  children: React.ReactNode,
+  href?: string,
+  className?: string,
+} ;
+
+export const NextLink = ({
+  href, children, className, ...props
+}: Props): JSX.Element => {
+
+  const { data: siteUrl } = useSiteUrl();
+
+  // when href is an anchor link
+  if (href == null || isAnchorLink(href)) {
+    return <a href={href} className={className}>{children}</a>;
+  }
+
+  if (isExternalLink(href, siteUrl)) {
+    return (
+      <a href={href} className={className} target="_blank" rel="noopener noreferrer">
+        {children}&nbsp;<i className='icon-share-alt small'></i>
+      </a>
+    );
+  }
+
+  return (
+    <Link {...props} href={href}>
+      <a className={className}>{children}</a>
+    </Link>
+  );
+};

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

@@ -15,6 +15,7 @@ import {
   usePageDuplicateModal, usePageRenameModal, usePageDeleteModal,
 } from '~/stores/modal';
 import { useDescendantsPageListForCurrentPathTermManager, usePageTreeTermManager } from '~/stores/page-listing';
+import { useSearchResultOptions } from '~/stores/renderer';
 import { useFullTextSearchTermManager } from '~/stores/search';
 
 
@@ -119,8 +120,7 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
   const { open: openRenameModal } = usePageRenameModal();
   const { open: openDeleteModal } = usePageDeleteModal();
 
-  const growiRenderer = appContainer.getRenderer('searchresult');
-
+  const { data: rendererOptions } = useSearchResultOptions();
 
   const duplicateItemClickedHandler = useCallback(async(pageToDuplicate) => {
     // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -193,8 +193,8 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
     );
   }, [page, showPageControlDropdown, forceHideMenuItems, duplicateItemClickedHandler, renameItemClickedHandler, deleteItemClickedHandler]);
 
-  // return if page is null
-  if (page == null) return <></>;
+  // return if page or growiRenderer is null
+  if (page == null || rendererOptions == null) return <></>;
 
   return (
     <div key={page._id} data-testid="search-result-content" className="search-result-content grw-page-path-text-muted-container d-flex flex-column">
@@ -208,7 +208,7 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
       </div>
       <div className="search-result-content-body-container" ref={scrollElementRef}>
         <RevisionLoader
-          growiRenderer={growiRenderer}
+          rendererOptions={rendererOptions}
           pageId={page._id}
           pagePath={page.path}
           revisionId={page.revision}

+ 1 - 1
packages/app/src/components/Sidebar.tsx

@@ -15,6 +15,7 @@ import {
 } from '~/stores/ui';
 
 import DrawerToggler from './Navbar/DrawerToggler';
+import { NavigationResizeHexagon } from './Sidebar/NavigationResizeHexagon';
 import { SidebarNav } from './Sidebar/SidebarNav';
 import { StickyStretchableScrollerProps } from './StickyStretchableScroller';
 
@@ -86,7 +87,6 @@ const SidebarContentsWrapper = () => {
 
 
 const Sidebar = (): JSX.Element => {
-  const NavigationResizeHexagon = dynamic(() => import('./Sidebar/NavigationResizeHexagon').then(mod => mod.NavigationResizeHexagon), { ssr: false });
 
   // const { data: isDrawerMode } = useDrawerMode(); Todo Universalize
   const isDrawerMode = false; // dummy

+ 8 - 10
packages/app/src/components/Sidebar/CustomSidebar.tsx

@@ -7,6 +7,7 @@ import { useSWRxPageByPath } from '~/stores/page';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import RevisionRenderer from '../Page/RevisionRenderer';
 import { IRevision } from '~/interfaces/revision';
+import { useCustomSidebarOptions } from '~/stores/renderer';
 
 const logger = loggerFactory('growi:cli:CustomSidebar');
 
@@ -27,12 +28,14 @@ type Props = {
 
 const CustomSidebar: FC<Props> = (props: Props) => {
 
-  const { appContainer } = props;
-
-  const renderer = appContainer.getRenderer('sidebar');
+  const { data: rendererOptions } = useCustomSidebarOptions();
 
   const { data: page, error, mutate } = useSWRxPageByPath('/Sidebar');
 
+  if (rendererOptions == null) {
+    return <></>;
+  }
+
   const isLoading = page === undefined && error == null;
   const markdown = (page?.revision as IRevision | undefined)?.body;
 
@@ -60,7 +63,7 @@ const CustomSidebar: FC<Props> = (props: Props) => {
         (!isLoading && markdown != null) && (
           <div className="p-3">
             <RevisionRenderer
-              growiRenderer={renderer}
+              rendererOptions={rendererOptions}
               markdown={markdown}
               pagePath="/Sidebar"
               additionalClassName="grw-custom-sidebar-content"
@@ -78,9 +81,4 @@ const CustomSidebar: FC<Props> = (props: Props) => {
   );
 };
 
-/**
- * Wrapper component for using unstated
- */
-const CustomSidebarWrapper = withUnstatedContainers(CustomSidebar, [AppContainer]);
-
-export default CustomSidebarWrapper;
+export default CustomSidebar;

+ 7 - 3
packages/app/src/components/Sidebar/SidebarNav.tsx

@@ -1,5 +1,7 @@
 import React, { FC, memo, useCallback } from 'react';
 
+import Link from 'next/link';
+
 import { useUserUISettings } from '~/client/services/user-ui-settings';
 import { SidebarContentsType } from '~/interfaces/ui';
 import { useCurrentUser } from '~/stores/context';
@@ -60,9 +62,11 @@ const SecondaryItem: FC<SecondaryItemProps> = memo((props: SecondaryItemProps) =
   const { iconName, href, isBlank } = props;
 
   return (
-    <a href={href} className="d-block btn btn-primary" target={`${isBlank ? '_blank' : ''}`}>
-      <i className="material-icons">{iconName}</i>
-    </a>
+    <Link href={href}>
+      <a className="d-block btn btn-primary" target={`${isBlank ? '_blank' : ''}`}>
+        <i className="material-icons">{iconName}</i>
+      </a>
+    </Link>
   );
 });
 SecondaryItem.displayName = 'SecondaryItem';

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

@@ -0,0 +1,14 @@
+@use '~/styles/bootstrap/init' as bs;
+
+.btn-subscribe {
+  &:global {
+    @include bs.button-outline-variant(rgba(bs.$secondary, 50%), bs.$success, rgba(lighten(bs.$success, 10%), 0.15), rgba(lighten(bs.$success, 10%), 0.5));
+    &:not(:disabled):not(.disabled):active,
+    &:not(:disabled):not(.disabled).active {
+      color: lighten(bs.$success, 15%);
+    }
+    &:not(:disabled):not(.disabled):not(:hover) {
+      background-color: transparent;
+    }
+  }
+}

+ 3 - 1
packages/app/src/components/SubscribeButton.tsx

@@ -5,6 +5,8 @@ import { UncontrolledTooltip } from 'reactstrap';
 
 import { SubscriptionStatusType } from '~/interfaces/subscription';
 
+import styles from './SubscribeButton.module.scss';
+
 
 type Props = {
   isGuestUser?: boolean,
@@ -35,7 +37,7 @@ const SubscribeButton: FC<Props> = (props: Props) => {
         type="button"
         id="subscribe-button"
         onClick={props.onClick}
-        className={`shadow-none btn btn-subscribe border-0
+        className={`shadow-none btn ${styles['btn-subscribe']} border-0
           ${isSubscribing ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`}
       >
         <i className={`fa ${isSubscribing ? 'fa-bell' : 'fa-bell-slash-o'}`}></i>

+ 10 - 8
packages/app/src/components/TableOfContents.jsx

@@ -53,15 +53,17 @@ const TableOfContents = (props) => {
     addSmoothScrollEvent(anchorsInToc, blinkElem);
   }, [tocHtml]);
 
+  // == TODO: render ToC without globalEmitter -- Yuki Takei
+  //
   // set handler to render ToC
-  useEffect(() => {
-    const handler = html => setTocHtml(html);
-    window.globalEmitter.on('renderTocHtml', handler);
-
-    return function cleanup() {
-      window.globalEmitter.removeListener('renderTocHtml', handler);
-    };
-  }, []);
+  // useEffect(() => {
+  //   const handler = html => setTocHtml(html);
+  //   globalEmitter.on('renderTocHtml', handler);
+
+  //   return function cleanup() {
+  //     globalEmitter.removeListener('renderTocHtml', handler);
+  //   };
+  // }, [globalEmitter]);
 
   return (
     <StickyStretchableScroller

+ 33 - 0
packages/app/src/interfaces/activity.ts

@@ -123,6 +123,17 @@ const ACTION_ADMIN_GLOBAL_NOTIFICATION_SETTINGS_ENABLED = 'ADMIN_GLOBAL_NOTIFICA
 const ACTION_ADMIN_GLOBAL_NOTIFICATION_SETTINGS_DISABLED = 'ADMIN_GLOBAL_NOTIFICATION_SETTINGS_DISABLED';
 const ACTION_ADMIN_GLOBAL_NOTIFICATION_SETTINGS_DELETE = 'ADMIN_GLOBAL_NOTIFICATION_SETTINGS_DELETE';
 const ACTION_ADMIN_SLACK_WORKSPACE_CREATE = 'ADMIN_SLACK_WORKSPACE_CREATE';
+const ACTION_ADMIN_SLACK_WORKSPACE_DELETE = 'ADMIN_SLACK_WORKSPACE_DELETE';
+const ACTION_ADMIN_SLACK_BOT_TYPE_UPDATE = 'ADMIN_SLACK_BOT_TYPE_UPDATE';
+const ACTION_ADMIN_SLACK_BOT_TYPE_DELETE = 'ADMIN_SLACK_BOT_TYPE_UPDATE';
+const ACTION_ADMIN_SLACK_ACCESS_TOKEN_REGENERATE = 'ADMIN_SLACK_ACCESS_TOKEN_REGENERATE';
+const ACTION_ADMIN_SLACK_MAKE_APP_PRIMARY = 'ADMIN_SLACK_MAKE_APP_PRIMARY';
+const ACTION_ADMIN_SLACK_PERMISSION_UPDATE = 'ADMIN_SLACK_PERMISSION_UPDATE';
+const ACTION_ADMIN_SLACK_PROXY_URI_UPDATE = 'ADMIN_SLACK_PROXY_URI_UPDATE';
+const ACTION_ADMIN_SLACK_RELATION_TEST = 'ADMIN_SLACK_RELATION_TEST';
+const ACTION_ADMIN_SLACK_WITHOUT_PROXY_SETTINGS_UPDATE = 'ADMIN_SLACK_WITHOUT_PROXY_SETTINGS_UPDATE';
+const ACTION_ADMIN_SLACK_WITHOUT_PROXY_PERMISSION_UPDATE = 'ADMIN_SLACK_WITHOUT_PROXY_PERMISSION_UPDATE';
+const ACTION_ADMIN_SLACK_WITHOUT_PROXY_TEST = 'ADMIN_SLACK_WITHOUT_PROXY_TEST';
 const ACTION_ADMIN_SLACK_CONFIGURATION_SETTING_UPDATE = 'ADMIN_SLACK_CONFIGURATION_SETTING_UPDATE';
 const ACTION_ADMIN_USERS_INVITE = 'ADMIN_USERS_INVITE';
 const ACTION_ADMIN_USER_GROUP_CREATE = 'ADMIN_USER_GROUP_CREATE';
@@ -278,6 +289,17 @@ export const SupportedAction = {
   ACTION_ADMIN_GLOBAL_NOTIFICATION_SETTINGS_DISABLED,
   ACTION_ADMIN_GLOBAL_NOTIFICATION_SETTINGS_DELETE,
   ACTION_ADMIN_SLACK_WORKSPACE_CREATE,
+  ACTION_ADMIN_SLACK_WORKSPACE_DELETE,
+  ACTION_ADMIN_SLACK_BOT_TYPE_UPDATE,
+  ACTION_ADMIN_SLACK_BOT_TYPE_DELETE,
+  ACTION_ADMIN_SLACK_ACCESS_TOKEN_REGENERATE,
+  ACTION_ADMIN_SLACK_MAKE_APP_PRIMARY,
+  ACTION_ADMIN_SLACK_PERMISSION_UPDATE,
+  ACTION_ADMIN_SLACK_PROXY_URI_UPDATE,
+  ACTION_ADMIN_SLACK_RELATION_TEST,
+  ACTION_ADMIN_SLACK_WITHOUT_PROXY_SETTINGS_UPDATE,
+  ACTION_ADMIN_SLACK_WITHOUT_PROXY_PERMISSION_UPDATE,
+  ACTION_ADMIN_SLACK_WITHOUT_PROXY_TEST,
   ACTION_ADMIN_SLACK_CONFIGURATION_SETTING_UPDATE,
   ACTION_ADMIN_USERS_INVITE,
   ACTION_ADMIN_USER_GROUP_CREATE,
@@ -435,6 +457,17 @@ export const LargeActionGroup = {
   ACTION_ADMIN_GLOBAL_NOTIFICATION_SETTINGS_DISABLED,
   ACTION_ADMIN_GLOBAL_NOTIFICATION_SETTINGS_DELETE,
   ACTION_ADMIN_SLACK_WORKSPACE_CREATE,
+  ACTION_ADMIN_SLACK_WORKSPACE_DELETE,
+  ACTION_ADMIN_SLACK_BOT_TYPE_UPDATE,
+  ACTION_ADMIN_SLACK_BOT_TYPE_DELETE,
+  ACTION_ADMIN_SLACK_ACCESS_TOKEN_REGENERATE,
+  ACTION_ADMIN_SLACK_MAKE_APP_PRIMARY,
+  ACTION_ADMIN_SLACK_PERMISSION_UPDATE,
+  ACTION_ADMIN_SLACK_PROXY_URI_UPDATE,
+  ACTION_ADMIN_SLACK_RELATION_TEST,
+  ACTION_ADMIN_SLACK_WITHOUT_PROXY_SETTINGS_UPDATE,
+  ACTION_ADMIN_SLACK_WITHOUT_PROXY_PERMISSION_UPDATE,
+  ACTION_ADMIN_SLACK_WITHOUT_PROXY_TEST,
   ACTION_ADMIN_SLACK_CONFIGURATION_SETTING_UPDATE,
   ACTION_ADMIN_USERS_INVITE,
   ACTION_ADMIN_USER_GROUP_CREATE,

+ 4 - 3
packages/app/src/interfaces/global.ts

@@ -1,12 +1,13 @@
 import EventEmitter from 'events';
 
+import GrowiRenderer from '~/services/renderer/growi-renderer';
 import Xss from '~/services/xss';
 
 import { IGraphViewer } from './graph-viewer';
-import { IInterceptorManager } from './interceptor-manager';
 
 export type CustomWindow = Window
                          & typeof globalThis
-                         & { interceptorManager: IInterceptorManager }
                          & { globalEmitter: EventEmitter }
-                         & { GraphViewer: IGraphViewer };
+                         & { GraphViewer: IGraphViewer }
+                         & { growiRenderer: GrowiRenderer }
+                         & { previewRenderer: GrowiRenderer }; // TODO: Remove this code when reveal.js is omitted. see: https://github.com/weseek/growi/pull/6223

+ 20 - 0
packages/app/src/interfaces/services/renderer.ts

@@ -0,0 +1,20 @@
+import { XssOptionConfig } from '~/services/xss/xssOption';
+
+// export type GrowiHydratedEnv = {
+//   DRAWIO_URI: string | null,
+//   HACKMD_URI: string | null,
+//   NO_CDN: string | null,
+//   GROWI_CLOUD_URI: string | null,
+//   GROWI_APP_ID_FOR_GROWI_CLOUD: string | null,
+// }
+
+export type RendererConfig = {
+  isEnabledLinebreaks: boolean,
+  isEnabledLinebreaksInComments: boolean,
+  adminPreferredIndentSize: number,
+  isIndentSizeForced: boolean,
+  highlightJsStyleBorder: boolean
+
+  plantumlUri: string | null,
+  blockdiagUri: string | null,
+} & XssOptionConfig;

+ 2 - 1
packages/app/src/migrations/20180927102719-init-serverurl.js

@@ -1,6 +1,7 @@
+import { getMongoUri, mongoOptions } from '@growi/core';
 import mongoose from 'mongoose';
 
-import { getMongoUri, mongoOptions } from '@growi/core';
+// eslint-disable-next-line import/no-named-as-default
 import Config from '~/server/models/config';
 import loggerFactory from '~/utils/logger';
 

+ 2 - 1
packages/app/src/migrations/20190618055300-abolish-crowi-classic-auth.js

@@ -1,6 +1,7 @@
+import { getMongoUri, mongoOptions } from '@growi/core';
 import mongoose from 'mongoose';
 
-import { getMongoUri, mongoOptions } from '@growi/core';
+// eslint-disable-next-line import/no-named-as-default
 import Config from '~/server/models/config';
 import loggerFactory from '~/utils/logger';
 

+ 2 - 1
packages/app/src/migrations/20190618104011-add-config-app-installed.js

@@ -1,6 +1,7 @@
+import { getModelSafely, getMongoUri, mongoOptions } from '@growi/core';
 import mongoose from 'mongoose';
 
-import { getModelSafely, getMongoUri, mongoOptions } from '@growi/core';
+// eslint-disable-next-line import/no-named-as-default
 import Config from '~/server/models/config';
 import loggerFactory from '~/utils/logger';
 

+ 2 - 1
packages/app/src/migrations/20200420160390-remove-crowi-layout.js

@@ -1,6 +1,7 @@
+import { getMongoUri, mongoOptions } from '@growi/core';
 import mongoose from 'mongoose';
 
-import { getMongoUri, mongoOptions } from '@growi/core';
+// eslint-disable-next-line import/no-named-as-default
 import Config from '~/server/models/config';
 import loggerFactory from '~/utils/logger';
 

+ 2 - 1
packages/app/src/migrations/20200512005851-remove-behavior-type.js

@@ -1,6 +1,7 @@
+import { getMongoUri, mongoOptions } from '@growi/core';
 import mongoose from 'mongoose';
 
-import { getMongoUri, mongoOptions } from '@growi/core';
+// eslint-disable-next-line import/no-named-as-default
 import Config from '~/server/models/config';
 import loggerFactory from '~/utils/logger';
 

+ 2 - 1
packages/app/src/migrations/20200514001356-update-theme-color-for-dark.js

@@ -1,6 +1,7 @@
+import { getMongoUri, mongoOptions } from '@growi/core';
 import mongoose from 'mongoose';
 
-import { getMongoUri, mongoOptions } from '@growi/core';
+// eslint-disable-next-line import/no-named-as-default
 import Config from '~/server/models/config';
 import loggerFactory from '~/utils/logger';
 

+ 2 - 1
packages/app/src/migrations/20200620203632-normalize-locale-id.js

@@ -1,6 +1,7 @@
+import { getModelSafely, getMongoUri, mongoOptions } from '@growi/core';
 import mongoose from 'mongoose';
 
-import { getModelSafely, getMongoUri, mongoOptions } from '@growi/core';
+// eslint-disable-next-line import/no-named-as-default
 import Config from '~/server/models/config';
 import loggerFactory from '~/utils/logger';
 

+ 2 - 1
packages/app/src/migrations/20200827045151-remove-layout-setting.js

@@ -1,6 +1,7 @@
+import { getMongoUri, mongoOptions } from '@growi/core';
 import mongoose from 'mongoose';
 
-import { getMongoUri, mongoOptions } from '@growi/core';
+// eslint-disable-next-line import/no-named-as-default
 import Config from '~/server/models/config';
 import loggerFactory from '~/utils/logger';
 

+ 2 - 1
packages/app/src/migrations/20200828024025-copy-aws-setting.js

@@ -1,6 +1,7 @@
+import { getMongoUri, mongoOptions } from '@growi/core';
 import mongoose from 'mongoose';
 
-import { getMongoUri, mongoOptions } from '@growi/core';
+// eslint-disable-next-line import/no-named-as-default
 import Config from '~/server/models/config';
 import loggerFactory from '~/utils/logger';
 

+ 2 - 1
packages/app/src/migrations/20200901034313-update-mail-transmission.js

@@ -1,6 +1,7 @@
+import { getMongoUri, mongoOptions } from '@growi/core';
 import mongoose from 'mongoose';
 
-import { getMongoUri, mongoOptions } from '@growi/core';
+// eslint-disable-next-line import/no-named-as-default
 import Config from '~/server/models/config';
 import loggerFactory from '~/utils/logger';
 

+ 3 - 1
packages/app/src/migrations/20200903080025-remove-timeline-type.js.js

@@ -1,7 +1,9 @@
 import { getMongoUri, mongoOptions } from '@growi/core';
-import loggerFactory from '~/utils/logger';
 
+// eslint-disable-next-line import/no-named-as-default
 import Config from '~/server/models/config';
+import loggerFactory from '~/utils/logger';
+
 
 const logger = loggerFactory('growi:migrate:remove-timeline-type');
 

+ 3 - 1
packages/app/src/migrations/20200915035234-rename-s3-config.js

@@ -1,7 +1,9 @@
 import { getMongoUri, mongoOptions } from '@growi/core';
-import loggerFactory from '~/utils/logger';
 
+// eslint-disable-next-line import/no-named-as-default
 import Config from '~/server/models/config';
+import loggerFactory from '~/utils/logger';
+
 
 const logger = loggerFactory('growi:migrate:remove-timeline-type');
 

+ 2 - 1
packages/app/src/migrations/20210830074539-update-configs-for-slackbot.js

@@ -1,6 +1,7 @@
+import { getMongoUri, mongoOptions } from '@growi/core';
 import mongoose from 'mongoose';
 
-import { getMongoUri, mongoOptions } from '@growi/core';
+// eslint-disable-next-line import/no-named-as-default
 import Config from '~/server/models/config';
 import loggerFactory from '~/utils/logger';
 

Некоторые файлы не были показаны из-за большого количества измененных файлов