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

Merge branch 'support/apply-nextjs-2' into feat/apply-style-to-sidebar

yohei0125 3 лет назад
Родитель
Сommit
5c0d2dd18e
65 измененных файлов с 704 добавлено и 378 удалено
  1. 5 0
      packages/app/next.config.js
  2. 2 0
      packages/app/package.json
  3. 1 0
      packages/app/public/static/locales/en_US/translation.json
  4. 1 0
      packages/app/public/static/locales/ja_JP/translation.json
  5. 1 0
      packages/app/public/static/locales/zh_CN/translation.json
  6. 1 1
      packages/app/resource/locales/en_US/welcome.md
  7. 1 1
      packages/app/resource/locales/ja_JP/welcome.md
  8. 1 1
      packages/app/resource/locales/zh_CN/welcome.md
  9. 24 25
      packages/app/src/components/Admin/Customize/Customize.jsx
  10. 1 3
      packages/app/src/components/Admin/Customize/CustomizeCssSetting.tsx
  11. 1 3
      packages/app/src/components/Admin/Customize/CustomizeFunctionSetting.tsx
  12. 6 6
      packages/app/src/components/Admin/Customize/CustomizeTitle.jsx
  13. 22 20
      packages/app/src/components/Admin/LegacySlackIntegration/LegacySlackIntegration.jsx
  14. 1 1
      packages/app/src/components/Admin/LegacySlackIntegration/SlackConfiguration.jsx
  15. 7 11
      packages/app/src/components/Admin/SlackIntegration/CustomBotWithProxySettings.jsx
  16. 3 8
      packages/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySecretTokenSection.jsx
  17. 11 9
      packages/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySettings.jsx
  18. 3 9
      packages/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx
  19. 8 11
      packages/app/src/components/Admin/SlackIntegration/OfficialBotSettings.jsx
  20. 2 13
      packages/app/src/components/Admin/SlackIntegration/SlackIntegration.jsx
  21. 8 9
      packages/app/src/components/Admin/SlackIntegration/WithProxyAccordions.jsx
  22. 2 2
      packages/app/src/components/Admin/UserGroup/UserGroupDeleteModal.tsx
  23. 1 1
      packages/app/src/components/BookmarkButtons.tsx
  24. 1 1
      packages/app/src/components/Common/Dropdown/PageItemControl.tsx
  25. 23 23
      packages/app/src/components/InstallerForm.jsx
  26. 10 3
      packages/app/src/components/Layout/RawLayout.tsx
  27. 1 1
      packages/app/src/components/LikeButtons.tsx
  28. 5 3
      packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  29. 5 12
      packages/app/src/components/Navbar/GrowiNavbar.tsx
  30. 0 26
      packages/app/src/components/Navbar/GrowiSubNavigation.module.scss
  31. 5 3
      packages/app/src/components/Navbar/GrowiSubNavigation.tsx
  32. 8 4
      packages/app/src/components/Navbar/GrowiSubNavigationSwitcher.jsx
  33. 32 0
      packages/app/src/components/Navbar/GrowiSubNavigationSwitcher.module.scss
  34. 18 5
      packages/app/src/components/Navbar/SubNavButtons.tsx
  35. 12 10
      packages/app/src/components/Page/DisplaySwitcher.tsx
  36. 6 2
      packages/app/src/components/PagePathNav.tsx
  37. 9 12
      packages/app/src/components/Sidebar/CustomSidebar.tsx
  38. 2 5
      packages/app/src/components/Sidebar/SidebarContents.tsx
  39. 21 0
      packages/app/src/components/Skelton.tsx
  40. 1 1
      packages/app/src/components/SubscribeButton.tsx
  41. 13 15
      packages/app/src/components/SuspenseUtils.jsx
  42. 28 0
      packages/app/src/components/TableOfContents.module.scss
  43. 22 52
      packages/app/src/components/TableOfContents.tsx
  44. 2 2
      packages/app/src/pages/[[...path]].page.tsx
  45. 15 4
      packages/app/src/pages/admin/[[...path]].page.tsx
  46. 89 0
      packages/app/src/pages/installer.page.tsx
  47. 1 1
      packages/app/src/server/routes/index.js
  48. 65 8
      packages/app/src/services/renderer/renderer.tsx
  49. 5 0
      packages/app/src/stores/context.tsx
  50. 18 4
      packages/app/src/stores/renderer.tsx
  51. 1 1
      packages/app/src/stores/ui.tsx
  52. 0 4
      packages/app/src/styles/_layout.scss
  53. 0 2
      packages/app/src/styles/_variables.scss
  54. 1 1
      packages/app/src/styles/atoms/_buttons.scss
  55. 4 7
      packages/app/src/styles/bootstrap/_apply.scss
  56. 4 0
      packages/app/src/styles/bootstrap/_init.scss
  57. 1 0
      packages/app/src/styles/bootstrap/_variables.scss
  58. 5 2
      packages/app/src/styles/style-next.scss
  59. 7 0
      packages/app/src/styles/theme/_apply-colors-dark.scss
  60. 7 0
      packages/app/src/styles/theme/_apply-colors-light.scss
  61. 9 9
      packages/app/src/styles/theme/_apply-colors.scss
  62. 1 7
      packages/app/src/styles/theme/_reboot-bootstrap-colors.scss
  63. 9 11
      packages/app/src/styles/theme/_reboot-bootstrap-theme-colors.scss
  64. 2 2
      packages/core/src/utils/with-utils.ts
  65. 123 1
      yarn.lock

+ 5 - 0
packages/app/next.config.js

@@ -22,8 +22,13 @@ const setupWithTM = () => {
     'unified',
     'unified',
     'comma-separated-tokens',
     'comma-separated-tokens',
     'decode-named-character-reference',
     'decode-named-character-reference',
+    'html-void-elements',
+    'property-information',
     'space-separated-tokens',
     'space-separated-tokens',
     'trim-lines',
     'trim-lines',
+    'web-namespaces',
+    'vfile',
+    'zwitch',
     'emoticon',
     'emoticon',
     ...listPrefixedPackages(['remark-', 'rehype-', 'hast-', 'mdast-', 'micromark-', 'micromark-', 'unist-']),
     ...listPrefixedPackages(['remark-', 'rehype-', 'hast-', 'mdast-', 'micromark-', 'micromark-', 'unist-']),
   ];
   ];

+ 2 - 0
packages/app/package.json

@@ -154,6 +154,8 @@
     "react-multiline-clamp": "^2.0.0",
     "react-multiline-clamp": "^2.0.0",
     "reconnecting-websocket": "^4.4.0",
     "reconnecting-websocket": "^4.4.0",
     "redis": "^3.0.2",
     "redis": "^3.0.2",
+    "rehype-raw": "^6.1.1",
+    "rehype-sanitize": "^5.0.1",
     "rehype-slug": "^5.0.1",
     "rehype-slug": "^5.0.1",
     "rehype-toc": "^3.0.2",
     "rehype-toc": "^3.0.2",
     "remark-breaks": "^3.0.2",
     "remark-breaks": "^3.0.2",

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

@@ -30,6 +30,7 @@
   "New": "New",
   "New": "New",
   "Close": "Close",
   "Close": "Close",
   "Shortcuts": "Shortcuts",
   "Shortcuts": "Shortcuts",
+  "CustomSidebar": "Custom Sidebar",
   "eg": "e.g.",
   "eg": "e.g.",
   "add": "Add",
   "add": "Add",
   "Undo": "Undo",
   "Undo": "Undo",

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

@@ -30,6 +30,7 @@
   "New": "作成",
   "New": "作成",
   "Close": "閉じる",
   "Close": "閉じる",
   "Shortcuts": "ショートカット",
   "Shortcuts": "ショートカット",
+  "CustomSidebar": "カスタムサイドバー",
   "eg": "例:",
   "eg": "例:",
   "add": "追加",
   "add": "追加",
   "Undo": "元に戻す",
   "Undo": "元に戻す",

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

@@ -31,6 +31,7 @@
   "New": "新建",
   "New": "新建",
   "Close": "Close",
   "Close": "Close",
 	"Shortcuts": "快捷方式",
 	"Shortcuts": "快捷方式",
+  "CustomSidebar": "Custom Sidebar",
 	"eg": "e.g.",
 	"eg": "e.g.",
 	"add": "添加",
 	"add": "添加",
 	"Undo": "撤销",
 	"Undo": "撤销",

+ 1 - 1
packages/app/resource/locales/en_US/welcome.md

@@ -1,7 +1,7 @@
 # :tada: Welcome to GROWI
 # :tada: Welcome to GROWI
 
 
 [![GitHub Releases](https://img.shields.io/github/release/weseek/growi.svg)](https://github.com/weseek/growi/releases/latest)
 [![GitHub Releases](https://img.shields.io/github/release/weseek/growi.svg)](https://github.com/weseek/growi/releases/latest)
-[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)
+[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://github.com/weseek/growi/blob/master/LICENSE)
 
 
 GROWI is a Wiki for Individuals and Corporations | A knowledge base tool.
 GROWI is a Wiki for Individuals and Corporations | A knowledge base tool.
 Knowledge in companies, university laboratories, and clubs can be easily shared and anyone can edit the page.
 Knowledge in companies, university laboratories, and clubs can be easily shared and anyone can edit the page.

+ 1 - 1
packages/app/resource/locales/ja_JP/welcome.md

@@ -1,6 +1,6 @@
 # :tada: GROWI へようこそ
 # :tada: GROWI へようこそ
 [![GitHub Releases](https://img.shields.io/github/release/weseek/growi.svg)](https://github.com/weseek/growi/releases/latest)
 [![GitHub Releases](https://img.shields.io/github/release/weseek/growi.svg)](https://github.com/weseek/growi/releases/latest)
-[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)
+[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://github.com/weseek/growi/blob/master/LICENSE)
 
 
 GROWI は個人・法人向けの Wiki | ナレッジベースツールです。  
 GROWI は個人・法人向けの Wiki | ナレッジベースツールです。  
 会社や大学の研究室、サークルでのナレッジ情報を簡単に共有でき、作られたページは誰でも編集が可能です。
 会社や大学の研究室、サークルでのナレッジ情報を簡単に共有でき、作られたページは誰でも編集が可能です。

+ 1 - 1
packages/app/resource/locales/zh_CN/welcome.md

@@ -1,7 +1,7 @@
 # :tada: 欢迎来到GROWI
 # :tada: 欢迎来到GROWI
 
 
 [![GitHub Releases](https://img.shields.io/github/release/weseek/growi.svg)](https://github.com/weseek/growi/releases/latest)
 [![GitHub Releases](https://img.shields.io/github/release/weseek/growi.svg)](https://github.com/weseek/growi/releases/latest)
-[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)
+[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://github.com/weseek/growi/blob/master/LICENSE)
 
 
 GROWI是一个针对个人和公司的Wiki - 一个知识库工具。
 GROWI是一个针对个人和公司的Wiki - 一个知识库工具。
 公司、大学实验室和俱乐部的知识可以轻松共享,任何人都可以编辑页面。
 公司、大学实验室和俱乐部的知识可以轻松共享,任何人都可以编辑页面。

+ 24 - 25
packages/app/src/components/Admin/Customize/Customize.jsx

@@ -1,15 +1,13 @@
 
 
-import React, { Fragment } from 'react';
+import React from 'react';
 
 
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { toastError } from '~/client/util/apiNotification';
 import { toastError } from '~/client/util/apiNotification';
 import { toArrayIfNot } from '~/utils/array-utils';
 import { toArrayIfNot } from '~/utils/array-utils';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-import { withLoadingSppiner } from '../../SuspenseUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
 
 import CustomizeCssSetting from './CustomizeCssSetting';
 import CustomizeCssSetting from './CustomizeCssSetting';
@@ -24,33 +22,33 @@ import CustomizeTitle from './CustomizeTitle';
 
 
 const logger = loggerFactory('growi:services:AdminCustomizePage');
 const logger = loggerFactory('growi:services:AdminCustomizePage');
 
 
-let retrieveErrors = null;
+const retrieveErrors = null;
 function Customize(props) {
 function Customize(props) {
-  const { appContainer, adminCustomizeContainer } = props;
+  const { adminCustomizeContainer } = props;
 
 
-  if (adminCustomizeContainer.state.currentTheme === adminCustomizeContainer.dummyCurrentTheme) {
-    throw (async() => {
-      try {
-        await adminCustomizeContainer.retrieveCustomizeData();
-      }
-      catch (err) {
-        const errs = toArrayIfNot(err);
-        toastError(errs);
-        logger.error(errs);
-        retrieveErrors = errs;
-        adminCustomizeContainer.setState({ currentTheme: adminCustomizeContainer.dummyCurrentThemeForError });
-      }
-    })();
-  }
+  // if (adminCustomizeContainer.state.currentTheme === adminCustomizeContainer.dummyCurrentTheme) {
+  //   throw (async() => {
+  //     try {
+  //       await adminCustomizeContainer.retrieveCustomizeData();
+  //     }
+  //     catch (err) {
+  //       const errs = toArrayIfNot(err);
+  //       toastError(errs);
+  //       logger.error(errs);
+  //       retrieveErrors = errs;
+  //       adminCustomizeContainer.setState({ currentTheme: adminCustomizeContainer.dummyCurrentThemeForError });
+  //     }
+  //   })();
+  // }
 
 
-  if (adminCustomizeContainer.state.currentTheme === adminCustomizeContainer.dummyCurrentThemeForError) {
-    throw new Error(`${retrieveErrors.length} errors occured`);
-  }
+  // if (adminCustomizeContainer.state.currentTheme === adminCustomizeContainer.dummyCurrentThemeForError) {
+  //   throw new Error(`${retrieveErrors.length} errors occured`);
+  // }
 
 
   return (
   return (
     <div data-testid="admin-customize">
     <div data-testid="admin-customize">
       <div className="mb-5">
       <div className="mb-5">
-        <CustomizeLayoutSetting appContainer={appContainer} />
+        <CustomizeLayoutSetting />
       </div>
       </div>
       <div className="mb-5">
       <div className="mb-5">
         <CustomizeThemeSetting />
         <CustomizeThemeSetting />
@@ -67,6 +65,7 @@ function Customize(props) {
       <div className="mb-5">
       <div className="mb-5">
         <CustomizeTitle />
         <CustomizeTitle />
       </div>
       </div>
+      {/* TODO: show CustomizeHeaderSetting, CustomizeCssSetting and CustomizeScriptSetting by https://redmine.weseek.co.jp/issues/100534
       <div className="mb-5">
       <div className="mb-5">
         <CustomizeHeaderSetting />
         <CustomizeHeaderSetting />
       </div>
       </div>
@@ -76,14 +75,14 @@ function Customize(props) {
       <div className="mb-5">
       <div className="mb-5">
         <CustomizeScriptSetting />
         <CustomizeScriptSetting />
       </div>
       </div>
+    */}
     </div>
     </div>
   );
   );
 }
 }
 
 
-const CustomizePageWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(Customize), [AppContainer, AdminCustomizeContainer]);
+const CustomizePageWithUnstatedContainer = withUnstatedContainers(Customize, [AdminCustomizeContainer]);
 
 
 Customize.propTypes = {
 Customize.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
   adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
 };
 };
 
 

+ 1 - 3
packages/app/src/components/Admin/Customize/CustomizeCssSetting.tsx

@@ -4,7 +4,6 @@ import { useTranslation } from 'next-i18next';
 import { Card, CardBody } from 'reactstrap';
 import { Card, CardBody } from 'reactstrap';
 
 
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -12,7 +11,6 @@ import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import CustomCssEditor from '../CustomCssEditor';
 import CustomCssEditor from '../CustomCssEditor';
 
 
 type Props = {
 type Props = {
-  appContainer: AppContainer,
   adminCustomizeContainer: AdminCustomizeContainer
   adminCustomizeContainer: AdminCustomizeContainer
 }
 }
 
 
@@ -63,6 +61,6 @@ const CustomizeCssSetting = (props: Props): JSX.Element => {
 
 
 };
 };
 
 
-const CustomizeCssSettingWrapper = withUnstatedContainers(CustomizeCssSetting, [AppContainer, AdminCustomizeContainer]);
+const CustomizeCssSettingWrapper = withUnstatedContainers(CustomizeCssSetting, [AdminCustomizeContainer]);
 
 
 export default CustomizeCssSettingWrapper;
 export default CustomizeCssSettingWrapper;

+ 1 - 3
packages/app/src/components/Admin/Customize/CustomizeFunctionSetting.tsx

@@ -4,7 +4,6 @@ import { useTranslation } from 'next-i18next';
 import { Card, CardBody } from 'reactstrap';
 import { Card, CardBody } from 'reactstrap';
 
 
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -14,7 +13,6 @@ import CustomizeFunctionOption from './CustomizeFunctionOption';
 import PagingSizeUncontrolledDropdown from './PagingSizeUncontrolledDropdown';
 import PagingSizeUncontrolledDropdown from './PagingSizeUncontrolledDropdown';
 
 
 type Props = {
 type Props = {
-  appContainer: AppContainer,
   adminCustomizeContainer: AdminCustomizeContainer
   adminCustomizeContainer: AdminCustomizeContainer
 }
 }
 
 
@@ -158,6 +156,6 @@ const CustomizeFunctionSetting = (props: Props): JSX.Element => {
 
 
 };
 };
 
 
-const CustomizeFunctionSettingWrapper = withUnstatedContainers(CustomizeFunctionSetting, [AppContainer, AdminCustomizeContainer]);
+const CustomizeFunctionSettingWrapper = withUnstatedContainers(CustomizeFunctionSetting, [AdminCustomizeContainer]);
 
 
 export default CustomizeFunctionSettingWrapper;
 export default CustomizeFunctionSettingWrapper;

+ 6 - 6
packages/app/src/components/Admin/Customize/CustomizeTitle.jsx

@@ -1,15 +1,16 @@
 /* eslint-disable max-len */
 /* eslint-disable max-len */
 import React from 'react';
 import React from 'react';
-import PropTypes from 'prop-types';
+
 import { withTranslation } from 'next-i18next';
 import { withTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 import { Card, CardBody } from 'reactstrap';
 import { Card, CardBody } from 'reactstrap';
 
 
-import AppContainer from '~/client/services/AppContainer';
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
-import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
 
+import { withUnstatedContainers } from '../../UnstatedUtils';
+import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
+
 class CustomizeTitle extends React.Component {
 class CustomizeTitle extends React.Component {
 
 
   constructor(props) {
   constructor(props) {
@@ -85,11 +86,10 @@ class CustomizeTitle extends React.Component {
 
 
 }
 }
 
 
-const CustomizeTitleWrapper = withUnstatedContainers(CustomizeTitle, [AppContainer, AdminCustomizeContainer]);
+const CustomizeTitleWrapper = withUnstatedContainers(CustomizeTitle, [AdminCustomizeContainer]);
 
 
 CustomizeTitle.propTypes = {
 CustomizeTitle.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
   adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
 };
 };
 
 

+ 22 - 20
packages/app/src/components/Admin/LegacySlackIntegration/LegacySlackIntegration.jsx

@@ -1,38 +1,40 @@
-import React, { useMemo, useState } from 'react';
-import PropTypes from 'prop-types';
+import React, { useEffect, useMemo, useState } from 'react';
+
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 
+import AdminSlackIntegrationLegacyContainer from '~/client/services/AdminSlackIntegrationLegacyContainer';
+import { toastError } from '~/client/util/apiNotification';
+import { toArrayIfNot } from '~/utils/array-utils';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
-import { toastError } from '~/client/util/apiNotification';
-import { toArrayIfNot } from '~/utils/array-utils';
-import { withLoadingSppiner } from '../../SuspenseUtils';
 
 
-import AdminSlackIntegrationLegacyContainer from '~/client/services/AdminSlackIntegrationLegacyContainer';
 
 
 import SlackConfiguration from './SlackConfiguration';
 import SlackConfiguration from './SlackConfiguration';
 
 
 const logger = loggerFactory('growi:NotificationSetting');
 const logger = loggerFactory('growi:NotificationSetting');
 
 
-let retrieveErrors = null;
+const retrieveErrors = null;
 function LegacySlackIntegration(props) {
 function LegacySlackIntegration(props) {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const { adminSlackIntegrationLegacyContainer } = props;
   const { adminSlackIntegrationLegacyContainer } = props;
 
 
   if (adminSlackIntegrationLegacyContainer.state.webhookUrl === adminSlackIntegrationLegacyContainer.dummyWebhookUrl) {
   if (adminSlackIntegrationLegacyContainer.state.webhookUrl === adminSlackIntegrationLegacyContainer.dummyWebhookUrl) {
-    throw (async() => {
-      try {
-        await adminSlackIntegrationLegacyContainer.retrieveData();
-      }
-      catch (err) {
-        const errs = toArrayIfNot(err);
-        toastError(errs);
-        logger.error(errs);
-        retrieveErrors = errs;
-        adminSlackIntegrationLegacyContainer.setState({ webhookUrl: adminSlackIntegrationLegacyContainer.dummyWebhookUrlForError });
-      }
-    })();
+    // TODO: Omit AdminSlackIntegrationLegacyContainer by https://redmine.weseek.co.jp/issues/100947
+
+    // throw (async() => {
+    //   try {
+    //     await adminSlackIntegrationLegacyContainer.retrieveData();
+    //   }
+    //   catch (err) {
+    //     const errs = toArrayIfNot(err);
+    //     toastError(errs);
+    //     logger.error(errs);
+    //     retrieveErrors = errs;
+    //     adminSlackIntegrationLegacyContainer.setState({ webhookUrl: adminSlackIntegrationLegacyContainer.dummyWebhookUrlForError });
+    //   }
+    // })();
   }
   }
 
 
   if (adminSlackIntegrationLegacyContainer.state.webhookUrl === adminSlackIntegrationLegacyContainer.dummyWebhookUrlForError) {
   if (adminSlackIntegrationLegacyContainer.state.webhookUrl === adminSlackIntegrationLegacyContainer.dummyWebhookUrlForError) {
@@ -62,7 +64,7 @@ function LegacySlackIntegration(props) {
   );
   );
 }
 }
 
 
-const LegacySlackIntegrationWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(LegacySlackIntegration), [AdminSlackIntegrationLegacyContainer]);
+const LegacySlackIntegrationWithUnstatedContainer = withUnstatedContainers(LegacySlackIntegration, [AdminSlackIntegrationLegacyContainer]);
 
 
 LegacySlackIntegration.propTypes = {
 LegacySlackIntegration.propTypes = {
   adminSlackIntegrationLegacyContainer: PropTypes.instanceOf(AdminSlackIntegrationLegacyContainer).isRequired,
   adminSlackIntegrationLegacyContainer: PropTypes.instanceOf(AdminSlackIntegrationLegacyContainer).isRequired,

+ 1 - 1
packages/app/src/components/Admin/LegacySlackIntegration/SlackConfiguration.jsx

@@ -1,7 +1,7 @@
 import React from 'react';
 import React from 'react';
 
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 
 import AdminSlackIntegrationLegacyContainer from '~/client/services/AdminSlackIntegrationLegacyContainer';
 import AdminSlackIntegrationLegacyContainer from '~/client/services/AdminSlackIntegrationLegacyContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';

+ 7 - 11
packages/app/src/components/Admin/SlackIntegration/CustomBotWithProxySettings.jsx

@@ -1,14 +1,13 @@
 import React, { useState, useEffect, useCallback } from 'react';
 import React, { useState, useEffect, useCallback } from 'react';
 
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Delete, apiv3Put } from '~/client/util/apiv3-client';
 import { apiv3Delete, apiv3Put } from '~/client/util/apiv3-client';
+import { useAppTitle } from '~/stores/context';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
 
 
 import CustomBotWithProxyConnectionStatus from './CustomBotWithProxyConnectionStatus';
 import CustomBotWithProxyConnectionStatus from './CustomBotWithProxyConnectionStatus';
 import DeleteSlackBotSettingsModal from './DeleteSlackBotSettingsModal';
 import DeleteSlackBotSettingsModal from './DeleteSlackBotSettingsModal';
@@ -19,7 +18,7 @@ const logger = loggerFactory('growi:cli:SlackIntegration:CustomBotWithProxySetti
 
 
 const CustomBotWithProxySettings = (props) => {
 const CustomBotWithProxySettings = (props) => {
   const {
   const {
-    appContainer, slackAppIntegrations, proxyServerUri,
+    slackAppIntegrations, proxyServerUri,
     onClickAddSlackWorkspaceBtn, onPrimaryUpdated,
     onClickAddSlackWorkspaceBtn, onPrimaryUpdated,
     connectionStatuses, onUpdateTokens, onSubmitForm,
     connectionStatuses, onUpdateTokens, onSubmitForm,
   } = props;
   } = props;
@@ -27,6 +26,7 @@ const CustomBotWithProxySettings = (props) => {
   const [integrationIdToDelete, setIntegrationIdToDelete] = useState(null);
   const [integrationIdToDelete, setIntegrationIdToDelete] = useState(null);
   const [siteName, setSiteName] = useState('');
   const [siteName, setSiteName] = useState('');
   const { t } = useTranslation();
   const { t } = useTranslation();
+  const { data: appTitle } = useAppTitle();
 
 
   // componentDidUpdate
   // componentDidUpdate
   useEffect(() => {
   useEffect(() => {
@@ -86,9 +86,8 @@ const CustomBotWithProxySettings = (props) => {
   };
   };
 
 
   useEffect(() => {
   useEffect(() => {
-    const siteName = appContainer.config.crowi.title;
-    setSiteName(siteName);
-  }, [appContainer]);
+    setSiteName(appTitle);
+  }, [appTitle]);
 
 
   return (
   return (
     <>
     <>
@@ -183,14 +182,11 @@ const CustomBotWithProxySettings = (props) => {
   );
   );
 };
 };
 
 
-const CustomBotWithProxySettingsWrapper = withUnstatedContainers(CustomBotWithProxySettings, [AppContainer]);
-
 CustomBotWithProxySettings.defaultProps = {
 CustomBotWithProxySettings.defaultProps = {
   slackAppIntegrations: [],
   slackAppIntegrations: [],
 };
 };
 
 
 CustomBotWithProxySettings.propTypes = {
 CustomBotWithProxySettings.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   slackAppIntegrations: PropTypes.array,
   slackAppIntegrations: PropTypes.array,
   proxyServerUri: PropTypes.string,
   proxyServerUri: PropTypes.string,
   onClickAddSlackWorkspaceBtn: PropTypes.func,
   onClickAddSlackWorkspaceBtn: PropTypes.func,
@@ -201,4 +197,4 @@ CustomBotWithProxySettings.propTypes = {
   onUpdateTokens: PropTypes.func,
   onUpdateTokens: PropTypes.func,
 };
 };
 
 
-export default CustomBotWithProxySettingsWrapper;
+export default CustomBotWithProxySettings;

+ 3 - 8
packages/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySecretTokenSection.jsx

@@ -1,9 +1,8 @@
 import React, { useState, useEffect } from 'react';
 import React, { useState, useEffect } from 'react';
 
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Put } from '~/client/util/apiv3-client';
 import { apiv3Put } from '~/client/util/apiv3-client';
 
 
@@ -13,7 +12,7 @@ import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 
 const CustomBotWithoutProxySecretTokenSection = (props) => {
 const CustomBotWithoutProxySecretTokenSection = (props) => {
   const {
   const {
-    appContainer, slackSigningSecret, slackBotToken, slackSigningSecretEnv, slackBotTokenEnv, onUpdatedSecretToken,
+    slackSigningSecret, slackBotToken, slackSigningSecretEnv, slackBotTokenEnv, onUpdatedSecretToken,
   } = props;
   } = props;
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
@@ -113,11 +112,7 @@ const CustomBotWithoutProxySecretTokenSection = (props) => {
   );
   );
 };
 };
 
 
-const CustomBotWithoutProxySecretTokenSectionWrapper = withUnstatedContainers(CustomBotWithoutProxySecretTokenSection, [AppContainer]);
-
 CustomBotWithoutProxySecretTokenSection.propTypes = {
 CustomBotWithoutProxySecretTokenSection.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-
   onUpdatedSecretToken: PropTypes.func,
   onUpdatedSecretToken: PropTypes.func,
   slackSigningSecret: PropTypes.string,
   slackSigningSecret: PropTypes.string,
   slackSigningSecretEnv: PropTypes.string,
   slackSigningSecretEnv: PropTypes.string,
@@ -125,4 +120,4 @@ CustomBotWithoutProxySecretTokenSection.propTypes = {
   slackBotTokenEnv: PropTypes.string,
   slackBotTokenEnv: PropTypes.string,
 };
 };
 
 
-export default CustomBotWithoutProxySecretTokenSectionWrapper;
+export default CustomBotWithoutProxySecretTokenSection;

+ 11 - 9
packages/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySettings.jsx

@@ -1,20 +1,24 @@
 import React, { useState, useEffect } from 'react';
 import React, { useState, useEffect } from 'react';
+
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
-import AppContainer from '~/client/services/AppContainer';
+
+import { useAppTitle } from '~/stores/context';
+
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
-import CustomBotWithoutProxySettingsAccordion, { botInstallationStep } from './CustomBotWithoutProxySettingsAccordion';
+
 import CustomBotWithoutProxyConnectionStatus from './CustomBotWithoutProxyConnectionStatus';
 import CustomBotWithoutProxyConnectionStatus from './CustomBotWithoutProxyConnectionStatus';
+import CustomBotWithoutProxySettingsAccordion, { botInstallationStep } from './CustomBotWithoutProxySettingsAccordion';
 
 
 const CustomBotWithoutProxySettings = (props) => {
 const CustomBotWithoutProxySettings = (props) => {
-  const { appContainer, connectionStatuses } = props;
+  const { connectionStatuses } = props;
   const { t } = useTranslation();
   const { t } = useTranslation();
+  const { data: appTitle } = useAppTitle();
   const [siteName, setSiteName] = useState('');
   const [siteName, setSiteName] = useState('');
 
 
   useEffect(() => {
   useEffect(() => {
-    const siteName = appContainer.config.crowi.title;
-    setSiteName(siteName);
-  }, [appContainer]);
+    setSiteName(appTitle);
+  }, [appTitle]);
 
 
   const workspaceName = connectionStatuses[props.slackBotToken]?.workspaceName;
   const workspaceName = connectionStatuses[props.slackBotToken]?.workspaceName;
 
 
@@ -58,10 +62,8 @@ const CustomBotWithoutProxySettings = (props) => {
   );
   );
 };
 };
 
 
-const CustomBotWithoutProxySettingsWrapper = withUnstatedContainers(CustomBotWithoutProxySettings, [AppContainer]);
 
 
 CustomBotWithoutProxySettings.propTypes = {
 CustomBotWithoutProxySettings.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 
 
   slackSigningSecret: PropTypes.string,
   slackSigningSecret: PropTypes.string,
   slackSigningSecretEnv: PropTypes.string,
   slackSigningSecretEnv: PropTypes.string,
@@ -75,4 +77,4 @@ CustomBotWithoutProxySettings.propTypes = {
   eventActionsPermission: PropTypes.object,
   eventActionsPermission: PropTypes.object,
 };
 };
 
 
-export default CustomBotWithoutProxySettingsWrapper;
+export default CustomBotWithoutProxySettings;

+ 3 - 9
packages/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx

@@ -1,12 +1,10 @@
 import React, { useState } from 'react';
 import React, { useState } from 'react';
 
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 
-import AppContainer from '~/client/services/AppContainer';
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { apiv3Post } from '~/client/util/apiv3-client';
 
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
 import Accordion from '../Common/Accordion';
 import Accordion from '../Common/Accordion';
 
 
 import CustomBotWithoutProxySecretTokenSection from './CustomBotWithoutProxySecretTokenSection';
 import CustomBotWithoutProxySecretTokenSection from './CustomBotWithoutProxySecretTokenSection';
@@ -25,7 +23,7 @@ export const botInstallationStep = {
 
 
 const CustomBotWithoutProxySettingsAccordion = (props) => {
 const CustomBotWithoutProxySettingsAccordion = (props) => {
   const {
   const {
-    appContainer, activeStep, onTestConnectionInvoked,
+    activeStep, onTestConnectionInvoked,
     slackSigningSecret, slackBotToken, slackSigningSecretEnv, slackBotTokenEnv, commandPermission, eventActionsPermission,
     slackSigningSecret, slackBotToken, slackSigningSecretEnv, slackBotTokenEnv, commandPermission, eventActionsPermission,
   } = props;
   } = props;
   const successMessage = 'Successfully sent to Slack workspace.';
   const successMessage = 'Successfully sent to Slack workspace.';
@@ -190,12 +188,8 @@ const CustomBotWithoutProxySettingsAccordion = (props) => {
 };
 };
 
 
 
 
-const CustomBotWithoutProxySettingsAccordionWrapper = withUnstatedContainers(CustomBotWithoutProxySettingsAccordion, [AppContainer]);
-
-
 CustomBotWithoutProxySettingsAccordion.propTypes = {
 CustomBotWithoutProxySettingsAccordion.propTypes = {
   activeStep: PropTypes.oneOf(Object.values(botInstallationStep)).isRequired,
   activeStep: PropTypes.oneOf(Object.values(botInstallationStep)).isRequired,
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 
 
   onUpdatedSecretToken: PropTypes.func,
   onUpdatedSecretToken: PropTypes.func,
   onTestConnectionInvoked: PropTypes.func,
   onTestConnectionInvoked: PropTypes.func,
@@ -208,4 +202,4 @@ CustomBotWithoutProxySettingsAccordion.propTypes = {
   eventActionsPermission: PropTypes.object,
   eventActionsPermission: PropTypes.object,
 };
 };
 
 
-export default CustomBotWithoutProxySettingsAccordionWrapper;
+export default CustomBotWithoutProxySettingsAccordion;

+ 8 - 11
packages/app/src/components/Admin/SlackIntegration/OfficialBotSettings.jsx

@@ -1,17 +1,15 @@
 import React, { useState, useEffect, useCallback } from 'react';
 import React, { useState, useEffect, useCallback } from 'react';
 
 
 import { SlackbotType } from '@growi/slack';
 import { SlackbotType } from '@growi/slack';
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 
 
 
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Delete, apiv3Put } from '~/client/util/apiv3-client';
 import { apiv3Delete, apiv3Put } from '~/client/util/apiv3-client';
+import { useAppTitle } from '~/stores/context';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
-
 
 
 import CustomBotWithProxyConnectionStatus from './CustomBotWithProxyConnectionStatus';
 import CustomBotWithProxyConnectionStatus from './CustomBotWithProxyConnectionStatus';
 import DeleteSlackBotSettingsModal from './DeleteSlackBotSettingsModal';
 import DeleteSlackBotSettingsModal from './DeleteSlackBotSettingsModal';
@@ -22,13 +20,14 @@ const logger = loggerFactory('growi:cli:SlackIntegration:OfficialBotSettings');
 
 
 const OfficialBotSettings = (props) => {
 const OfficialBotSettings = (props) => {
   const {
   const {
-    appContainer, slackAppIntegrations,
+    slackAppIntegrations,
     onClickAddSlackWorkspaceBtn, onPrimaryUpdated,
     onClickAddSlackWorkspaceBtn, onPrimaryUpdated,
     connectionStatuses, onUpdateTokens, onSubmitForm,
     connectionStatuses, onUpdateTokens, onSubmitForm,
   } = props;
   } = props;
   const [siteName, setSiteName] = useState('');
   const [siteName, setSiteName] = useState('');
   const [integrationIdToDelete, setIntegrationIdToDelete] = useState(null);
   const [integrationIdToDelete, setIntegrationIdToDelete] = useState(null);
   const { t } = useTranslation();
   const { t } = useTranslation();
+  const { data: appTitle } = useAppTitle();
 
 
   const addSlackAppIntegrationHandler = async() => {
   const addSlackAppIntegrationHandler = async() => {
     if (onClickAddSlackWorkspaceBtn != null) {
     if (onClickAddSlackWorkspaceBtn != null) {
@@ -69,10 +68,10 @@ const OfficialBotSettings = (props) => {
     }
     }
   };
   };
 
 
+
   useEffect(() => {
   useEffect(() => {
-    const siteName = appContainer.config.crowi.title;
-    setSiteName(siteName);
-  }, [appContainer]);
+    setSiteName(appTitle);
+  }, [appTitle]);
 
 
   return (
   return (
     <>
     <>
@@ -151,14 +150,12 @@ const OfficialBotSettings = (props) => {
   );
   );
 };
 };
 
 
-const OfficialBotSettingsWrapper = withUnstatedContainers(OfficialBotSettings, [AppContainer]);
 
 
 OfficialBotSettings.defaultProps = {
 OfficialBotSettings.defaultProps = {
   slackAppIntegrations: [],
   slackAppIntegrations: [],
 };
 };
 
 
 OfficialBotSettings.propTypes = {
 OfficialBotSettings.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 
 
   slackAppIntegrations: PropTypes.array,
   slackAppIntegrations: PropTypes.array,
   onClickAddSlackWorkspaceBtn: PropTypes.func,
   onClickAddSlackWorkspaceBtn: PropTypes.func,
@@ -169,4 +166,4 @@ OfficialBotSettings.propTypes = {
   onSubmitForm: PropTypes.func,
   onSubmitForm: PropTypes.func,
 };
 };
 
 
-export default OfficialBotSettingsWrapper;
+export default OfficialBotSettings;

+ 2 - 13
packages/app/src/components/Admin/SlackIntegration/SlackIntegration.jsx

@@ -1,18 +1,14 @@
 import React, { useState, useEffect, useCallback } from 'react';
 import React, { useState, useEffect, useCallback } from 'react';
 
 
 import { SlackbotType } from '@growi/slack';
 import { SlackbotType } from '@growi/slack';
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
 
 
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import {
 import {
   apiv3Delete, apiv3Get, apiv3Post, apiv3Put,
   apiv3Delete, apiv3Get, apiv3Post, apiv3Put,
 } from '~/client/util/apiv3-client';
 } from '~/client/util/apiv3-client';
 
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
-
 import BotTypeCard from './BotTypeCard';
 import BotTypeCard from './BotTypeCard';
 import ConfirmBotChangeModal from './ConfirmBotChangeModal';
 import ConfirmBotChangeModal from './ConfirmBotChangeModal';
 import CustomBotWithProxySettings from './CustomBotWithProxySettings';
 import CustomBotWithProxySettings from './CustomBotWithProxySettings';
@@ -23,9 +19,8 @@ import OfficialBotSettings from './OfficialBotSettings';
 
 
 const botTypes = Object.values(SlackbotType);
 const botTypes = Object.values(SlackbotType);
 
 
-const SlackIntegration = (props) => {
+const SlackIntegration = () => {
 
 
-  const { appContainer } = props;
   const { t } = useTranslation();
   const { t } = useTranslation();
   const [currentBotType, setCurrentBotType] = useState(null);
   const [currentBotType, setCurrentBotType] = useState(null);
   const [selectedBotType, setSelectedBotType] = useState(null);
   const [selectedBotType, setSelectedBotType] = useState(null);
@@ -256,10 +251,4 @@ const SlackIntegration = (props) => {
   );
   );
 };
 };
 
 
-const SlackIntegrationWrapper = withUnstatedContainers(SlackIntegration, [AppContainer]);
-
-SlackIntegration.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-};
-
-export default SlackIntegrationWrapper;
+export default SlackIntegration;

+ 8 - 9
packages/app/src/components/Admin/SlackIntegration/WithProxyAccordions.jsx

@@ -2,14 +2,14 @@
 import React, { useState, useCallback } from 'react';
 import React, { useState, useCallback } from 'react';
 
 
 import { SlackbotType } from '@growi/slack';
 import { SlackbotType } from '@growi/slack';
+import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
-import { useTranslation } from 'next-i18next';
 import { Tooltip } from 'reactstrap';
 import { Tooltip } from 'reactstrap';
 
 
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Put, apiv3Post } from '~/client/util/apiv3-client';
 import { apiv3Put, apiv3Post } from '~/client/util/apiv3-client';
+import { useSiteUrl } from '~/stores/context';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -145,7 +145,7 @@ const CustomCopyToClipBoard = (props) => {
 
 
 const GeneratingTokensAndRegisteringProxyServiceProcess = withUnstatedContainers((props) => {
 const GeneratingTokensAndRegisteringProxyServiceProcess = withUnstatedContainers((props) => {
   const { t } = useTranslation();
   const { t } = useTranslation();
-  const { appContainer, slackAppIntegrationId } = props;
+  const { slackAppIntegrationId } = props;
 
 
   const regenerateTokensHandler = async() => {
   const regenerateTokensHandler = async() => {
     try {
     try {
@@ -231,7 +231,7 @@ const GeneratingTokensAndRegisteringProxyServiceProcess = withUnstatedContainers
     </div>
     </div>
 
 
   );
   );
-}, [AppContainer]);
+}, []);
 
 
 const TestProcess = ({
 const TestProcess = ({
   slackAppIntegrationId, onSubmitForm, onSubmitFormFailed, isLatestConnectionSuccess,
   slackAppIntegrationId, onSubmitForm, onSubmitFormFailed, isLatestConnectionSuccess,
@@ -313,6 +313,7 @@ const TestProcess = ({
 
 
 const WithProxyAccordions = (props) => {
 const WithProxyAccordions = (props) => {
   const { t } = useTranslation();
   const { t } = useTranslation();
+  const { data: siteUrl } = useSiteUrl();
   const [isLatestConnectionSuccess, setIsLatestConnectionSuccess] = useState(false);
   const [isLatestConnectionSuccess, setIsLatestConnectionSuccess] = useState(false);
 
 
   const submitForm = () => {
   const submitForm = () => {
@@ -334,7 +335,7 @@ const WithProxyAccordions = (props) => {
     '②': {
     '②': {
       title: 'register_for_growi_official_bot_proxy_service',
       title: 'register_for_growi_official_bot_proxy_service',
       content: <GeneratingTokensAndRegisteringProxyServiceProcess
       content: <GeneratingTokensAndRegisteringProxyServiceProcess
-        growiUrl={props.appContainer.config.crowi.url}
+        growiUrl={siteUrl}
         slackAppIntegrationId={props.slackAppIntegrationId}
         slackAppIntegrationId={props.slackAppIntegrationId}
         tokenPtoG={props.tokenPtoG}
         tokenPtoG={props.tokenPtoG}
         tokenGtoP={props.tokenGtoP}
         tokenGtoP={props.tokenGtoP}
@@ -373,7 +374,7 @@ const WithProxyAccordions = (props) => {
     '③': {
     '③': {
       title: 'register_for_growi_custom_bot_proxy',
       title: 'register_for_growi_custom_bot_proxy',
       content: <GeneratingTokensAndRegisteringProxyServiceProcess
       content: <GeneratingTokensAndRegisteringProxyServiceProcess
-        growiUrl={props.appContainer.config.crowi.url}
+        growiUrl={siteUrl}
         slackAppIntegrationId={props.slackAppIntegrationId}
         slackAppIntegrationId={props.slackAppIntegrationId}
         tokenPtoG={props.tokenPtoG}
         tokenPtoG={props.tokenPtoG}
         tokenGtoP={props.tokenGtoP}
         tokenGtoP={props.tokenGtoP}
@@ -434,9 +435,7 @@ const WithProxyAccordions = (props) => {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const WithProxyAccordionsWrapper = withUnstatedContainers(WithProxyAccordions, [AppContainer]);
 WithProxyAccordions.propTypes = {
 WithProxyAccordions.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   botType: PropTypes.oneOf(Object.values(SlackbotType)).isRequired,
   botType: PropTypes.oneOf(Object.values(SlackbotType)).isRequired,
   slackAppIntegrationId: PropTypes.string.isRequired,
   slackAppIntegrationId: PropTypes.string.isRequired,
   tokenPtoG: PropTypes.string,
   tokenPtoG: PropTypes.string,
@@ -446,4 +445,4 @@ WithProxyAccordions.propTypes = {
   permissionsForSlackEventActions: PropTypes.object.isRequired,
   permissionsForSlackEventActions: PropTypes.object.isRequired,
 };
 };
 
 
-export default WithProxyAccordionsWrapper;
+export default WithProxyAccordions;

+ 2 - 2
packages/app/src/components/Admin/UserGroup/UserGroupDeleteModal.tsx

@@ -135,7 +135,7 @@ const UserGroupDeleteModal: FC<Props> = (props: Props) => {
         onChange={handleActionChange}
         onChange={handleActionChange}
       >
       >
         <option value="" disabled>{t('admin:user_group_management.delete_modal.dropdown_desc')}</option>
         <option value="" disabled>{t('admin:user_group_management.delete_modal.dropdown_desc')}</option>
-        {...options}
+        {options}
       </select>
       </select>
     );
     );
   }, [availableOptions, actionName, handleActionChange, t]);
   }, [availableOptions, actionName, handleActionChange, t]);
@@ -164,7 +164,7 @@ const UserGroupDeleteModal: FC<Props> = (props: Props) => {
         onChange={handleGroupChange}
         onChange={handleGroupChange}
       >
       >
         <option value="" disabled>{defaultOptionText}</option>
         <option value="" disabled>{defaultOptionText}</option>
-        {...options}
+        {options}
       </select>
       </select>
     );
     );
   }, [deleteUserGroup, userGroups, t, actionName, transferToUserGroupId, handleGroupChange]);
   }, [deleteUserGroup, userGroups, t, actionName, transferToUserGroupId, handleGroupChange]);

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

@@ -57,7 +57,7 @@ const BookmarkButtons: FC<Props> = (props: Props) => {
         type="button"
         type="button"
         id="bookmark-button"
         id="bookmark-button"
         onClick={handleClick}
         onClick={handleClick}
-        className={`shadow-none btn ${styles['btn-bookmark']} border-0
+        className={`shadow-none btn btn-bookmark ${styles['btn-bookmark']} border-0
           ${isBookmarked ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`}
           ${isBookmarked ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`}
       >
       >
         <i className={`fa ${isBookmarked ? 'fa-bookmark' : 'fa-bookmark-o'}`}></i>
         <i className={`fa ${isBookmarked ? 'fa-bookmark' : 'fa-bookmark-o'}`}></i>

+ 1 - 1
packages/app/src/components/Common/Dropdown/PageItemControl.tsx

@@ -338,7 +338,7 @@ export const PageItemControlSubstance = (props: PageItemControlSubstanceProps):
 };
 };
 
 
 
 
-type PageItemControlProps = CommonProps & {
+export type PageItemControlProps = CommonProps & {
   pageId?: string,
   pageId?: string,
   children?: React.ReactNode,
   children?: React.ReactNode,
   operationProcessData?: IPageOperationProcessData,
   operationProcessData?: IPageOperationProcessData,

+ 23 - 23
packages/app/src/components/InstallerForm.jsx

@@ -1,10 +1,10 @@
 import React from 'react';
 import React from 'react';
 
 
 import i18next from 'i18next';
 import i18next from 'i18next';
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 
-import { localeMetadatas } from '~/client/util/i18n';
+// import { localeMetadatas } from '~/client/util/i18n';
 import { useCsrfToken } from '~/stores/context';
 import { useCsrfToken } from '~/stores/context';
 
 
 class InstallerForm extends React.Component {
 class InstallerForm extends React.Component {
@@ -17,31 +17,31 @@ class InstallerForm extends React.Component {
       isSubmittingDisabled: false,
       isSubmittingDisabled: false,
       selectedLang: {},
       selectedLang: {},
     };
     };
-    // this.checkUserName = this.checkUserName.bind(this);
+    this.checkUserName = this.checkUserName.bind(this);
 
 
     this.submitHandler = this.submitHandler.bind(this);
     this.submitHandler = this.submitHandler.bind(this);
   }
   }
 
 
-  UNSAFE_componentWillMount() {
-    const meta = localeMetadatas.find(v => v.id === i18next.language);
-    if (meta == null) {
-      return this.setState({ selectedLang: localeMetadatas[0] });
-    }
-    this.setState({ selectedLang: meta });
-  }
-
-  // checkUserName(event) {
-  //   const axios = require('axios').create({
-  //     headers: {
-  //       'Content-Type': 'application/json',
-  //       'X-Requested-With': 'XMLHttpRequest',
-  //     },
-  //     responseType: 'json',
-  //   });
-  //   axios.get('/_api/v3/check-username', { params: { username: event.target.value } })
-  //     .then((res) => { return this.setState({ isValidUserName: res.data.valid }) });
+  // UNSAFE_componentWillMount() {
+  //   const meta = localeMetadatas.find(v => v.id === i18next.language);
+  //   if (meta == null) {
+  //     return this.setState({ selectedLang: localeMetadatas[0] });
+  //   }
+  //   this.setState({ selectedLang: meta });
   // }
   // }
 
 
+  checkUserName(event) {
+    const axios = require('axios').create({
+      headers: {
+        'Content-Type': 'application/json',
+        'X-Requested-With': 'XMLHttpRequest',
+      },
+      responseType: 'json',
+    });
+    axios.get('/_api/v3/check-username', { params: { username: event.target.value } })
+      .then((res) => { return this.setState({ isValidUserName: res.data.valid }) });
+  }
+
   changeLanguage(meta) {
   changeLanguage(meta) {
     i18next.changeLanguage(meta.id);
     i18next.changeLanguage(meta.id);
     this.setState({ selectedLang: meta });
     this.setState({ selectedLang: meta });
@@ -97,7 +97,7 @@ class InstallerForm extends React.Component {
                   value={this.state.selectedLang.id}
                   value={this.state.selectedLang.id}
                   name="registerForm[app:globalLang]"
                   name="registerForm[app:globalLang]"
                 />
                 />
-                <div className="dropdown-menu" aria-labelledby="dropdownLanguage">
+                {/* <div className="dropdown-menu" aria-labelledby="dropdownLanguage">
                   {
                   {
                     localeMetadatas.map(meta => (
                     localeMetadatas.map(meta => (
                       <button
                       <button
@@ -111,7 +111,7 @@ class InstallerForm extends React.Component {
                       </button>
                       </button>
                     ))
                     ))
                   }
                   }
-                </div>
+                </div> */}
               </div>
               </div>
             </div>
             </div>
 
 

+ 10 - 3
packages/app/src/components/Layout/RawLayout.tsx

@@ -1,9 +1,9 @@
-import React, { ReactNode } from 'react';
+import React, { ReactNode, useEffect, useState } from 'react';
 
 
-import { useTheme } from 'next-themes';
 import Head from 'next/head';
 import Head from 'next/head';
 
 
 import { useGrowiTheme } from '~/stores/context';
 import { useGrowiTheme } from '~/stores/context';
+import { Themes, useNextThemes } from '~/stores/use-next-themes';
 
 
 import { ThemeProvider } from '../Theme/utils/ThemeProvider';
 import { ThemeProvider } from '../Theme/utils/ThemeProvider';
 
 
@@ -22,7 +22,14 @@ export const RawLayout = ({ children, title, className }: Props): JSX.Element =>
   const { data: growiTheme } = useGrowiTheme();
   const { data: growiTheme } = useGrowiTheme();
 
 
   // get color scheme from next-themes
   // get color scheme from next-themes
-  const { resolvedTheme: colorScheme } = useTheme();
+  const { resolvedTheme } = useNextThemes();
+
+  const [colorScheme, setColorScheme] = useState<Themes|undefined>(undefined);
+
+  // set colorScheme in CSR
+  useEffect(() => {
+    setColorScheme(resolvedTheme as Themes);
+  }, [resolvedTheme]);
 
 
   return (
   return (
     <>
     <>

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

@@ -50,7 +50,7 @@ const LikeButtons: FC<LikeButtonsProps> = (props: LikeButtonsProps) => {
         type="button"
         type="button"
         id="like-button"
         id="like-button"
         onClick={onLikeClicked}
         onClick={onLikeClicked}
-        className={`shadow-none btn ${styles['btn-like']} border-0
+        className={`shadow-none btn btn-like ${styles['btn-like']} border-0
             ${isLiked ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`}
             ${isLiked ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`}
       >
       >
         <i className={`fa ${isLiked ? 'fa-heart' : 'fa-heart-o'}`}></i>
         <i className={`fa ${isLiked ? 'fa-heart' : 'fa-heart-o'}`}></i>

+ 5 - 3
packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -1,7 +1,9 @@
 import React, { useState, useEffect, useCallback } from 'react';
 import React, { useState, useEffect, useCallback } from 'react';
 
 
+
 import { isPopulated } from '@growi/core';
 import { isPopulated } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
+import dynamic from 'next/dynamic';
 import { DropdownItem } from 'reactstrap';
 import { DropdownItem } from 'reactstrap';
 
 
 import { exportAsMarkdown } from '~/client/services/page-operation';
 import { exportAsMarkdown } from '~/client/services/page-operation';
@@ -26,16 +28,14 @@ import {
   useIsAbleToShowPageEditorModeManager, useIsAbleToShowPageAuthors,
   useIsAbleToShowPageEditorModeManager, useIsAbleToShowPageAuthors,
 } from '~/stores/ui';
 } from '~/stores/ui';
 
 
-import { AdditionalMenuItemsRendererProps } from '../Common/Dropdown/PageItemControl';
 import CreateTemplateModal from '../CreateTemplateModal';
 import CreateTemplateModal from '../CreateTemplateModal';
 import AttachmentIcon from '../Icons/AttachmentIcon';
 import AttachmentIcon from '../Icons/AttachmentIcon';
 import HistoryIcon from '../Icons/HistoryIcon';
 import HistoryIcon from '../Icons/HistoryIcon';
 import PresentationIcon from '../Icons/PresentationIcon';
 import PresentationIcon from '../Icons/PresentationIcon';
 import ShareLinkIcon from '../Icons/ShareLinkIcon';
 import ShareLinkIcon from '../Icons/ShareLinkIcon';
-
+import { Skelton } from '../Skelton';
 
 
 import { GrowiSubNavigation } from './GrowiSubNavigation';
 import { GrowiSubNavigation } from './GrowiSubNavigation';
-import PageEditorModeManager from './PageEditorModeManager';
 import { SubNavButtons } from './SubNavButtons';
 import { SubNavButtons } from './SubNavButtons';
 
 
 
 
@@ -151,6 +151,8 @@ type GrowiContextualSubNavigationProps = {
 
 
 const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps): JSX.Element => {
 const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps): JSX.Element => {
 
 
+  const PageEditorModeManager = dynamic(() => import('./PageEditorModeManager'), { ssr: false, loading: () => <Skelton width={208} height={32.49} /> });
+
   const { data: currentPage, mutate: mutateCurrentPage } = useSWRxCurrentPage();
   const { data: currentPage, mutate: mutateCurrentPage } = useSWRxCurrentPage();
   const path = currentPage?.path;
   const path = currentPage?.path;
 
 

+ 5 - 12
packages/app/src/components/Navbar/GrowiNavbar.tsx

@@ -18,19 +18,12 @@ import { useIsDeviceSmallerThanMd } from '~/stores/ui';
 import { HasChildren } from '../../interfaces/common';
 import { HasChildren } from '../../interfaces/common';
 import GrowiLogo from '../Icons/GrowiLogo';
 import GrowiLogo from '../Icons/GrowiLogo';
 
 
+import { GlobalSearchProps } from './GlobalSearch';
 import PersonalDropdown from './PersonalDropdown';
 import PersonalDropdown from './PersonalDropdown';
 
 
 import styles from './GrowiNavbar.module.scss';
 import styles from './GrowiNavbar.module.scss';
-import { GlobalSearchProps } from './GlobalSearch';
 
 
 
 
-const ShowSkeltonInSSR = memo(({ children }: HasChildren): JSX.Element => {
-  return isServer()
-    ? <></>
-    : <>{children}</>;
-});
-ShowSkeltonInSSR.displayName = 'ShowSkeltonInSSR';
-
 const NavbarRight = memo((): JSX.Element => {
 const NavbarRight = memo((): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
@@ -53,7 +46,7 @@ const NavbarRight = memo((): JSX.Element => {
     return (
     return (
       <>
       <>
         <li className="nav-item">
         <li className="nav-item">
-          <ShowSkeltonInSSR><InAppNotificationDropdown /></ShowSkeltonInSSR>
+          <InAppNotificationDropdown />
         </li>
         </li>
 
 
         <li className="nav-item d-none d-md-block">
         <li className="nav-item d-none d-md-block">
@@ -70,11 +63,11 @@ const NavbarRight = memo((): JSX.Element => {
         </li>
         </li>
 
 
         <li className="grw-apperance-mode-dropdown nav-item dropdown">
         <li className="grw-apperance-mode-dropdown nav-item dropdown">
-          <ShowSkeltonInSSR><AppearanceModeDropdown isAuthenticated={isAuthenticated} /></ShowSkeltonInSSR>
+          <AppearanceModeDropdown isAuthenticated={isAuthenticated} />
         </li>
         </li>
 
 
         <li className="grw-personal-dropdown nav-item dropdown dropdown-toggle dropdown-toggle-no-caret" data-testid="grw-personal-dropdown">
         <li className="grw-personal-dropdown nav-item dropdown dropdown-toggle dropdown-toggle-no-caret" data-testid="grw-personal-dropdown">
-          <ShowSkeltonInSSR><PersonalDropdown /></ShowSkeltonInSSR>
+          <PersonalDropdown />
         </li>
         </li>
       </>
       </>
     );
     );
@@ -84,7 +77,7 @@ const NavbarRight = memo((): JSX.Element => {
     return (
     return (
       <>
       <>
         <li className="grw-apperance-mode-dropdown nav-item dropdown">
         <li className="grw-apperance-mode-dropdown nav-item dropdown">
-          <ShowSkeltonInSSR><AppearanceModeDropdown isAuthenticated={isAuthenticated} /></ShowSkeltonInSSR>
+          <AppearanceModeDropdown isAuthenticated={isAuthenticated} />
         </li>
         </li>
 
 
         <li id="login-user" className="nav-item"><a className="nav-link" href="/login">Login</a></li>;
         <li id="login-user" className="nav-item"><a className="nav-link" href="/login">Login</a></li>;

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

@@ -171,29 +171,3 @@
     box-shadow: 0px 0px 6px 3px rgba(black, 0.15);
     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%);
-    }
-  }
-}

+ 5 - 3
packages/app/src/components/Navbar/GrowiSubNavigation.tsx

@@ -8,12 +8,13 @@ import {
   EditorMode, useEditorMode,
   EditorMode, useEditorMode,
 } from '~/stores/ui';
 } from '~/stores/ui';
 
 
-import TagLabels from '../Page/TagLabels';
-// import PagePathNav from '../PagePathNav';
+import PagePathNav from '../PagePathNav';
+import { Skelton } from '../Skelton';
 
 
 import AuthorInfo from './AuthorInfo';
 import AuthorInfo from './AuthorInfo';
 import DrawerToggler from './DrawerToggler';
 import DrawerToggler from './DrawerToggler';
 
 
+
 import styles from './GrowiSubNavigation.module.scss';
 import styles from './GrowiSubNavigation.module.scss';
 
 
 
 
@@ -37,7 +38,8 @@ type Props = {
 
 
 export const GrowiSubNavigation = (props: Props): JSX.Element => {
 export const GrowiSubNavigation = (props: Props): JSX.Element => {
 
 
-  const PagePathNav = dynamic(() => import('../PagePathNav'), { ssr: false });
+  const TagLabels = dynamic(() => import('../Page/TagLabels'), { ssr: false, loading: () => <Skelton width={124.5} height={21.99} /> });
+  const AuthorInfo = dynamic(() => import('./AuthorInfo'), { ssr: false, loading: () => <Skelton width={148.32} height={32.84} /> });
 
 
   const { data: editorMode } = useEditorMode();
   const { data: editorMode } = useEditorMode();
 
 

+ 8 - 4
packages/app/src/components/Navbar/GrowiSubNavigationSwitcher.jsx

@@ -1,16 +1,18 @@
 import React, {
 import React, {
   useMemo, useState, useRef, useEffect, useCallback,
   useMemo, useState, useRef, useEffect, useCallback,
 } from 'react';
 } from 'react';
-import PropTypes from 'prop-types';
 
 
+import PropTypes from 'prop-types';
 import StickyEvents from 'sticky-events';
 import StickyEvents from 'sticky-events';
 import { debounce } from 'throttle-debounce';
 import { debounce } from 'throttle-debounce';
 
 
-import loggerFactory from '~/utils/logger';
 import { useSidebarCollapsed } from '~/stores/ui';
 import { useSidebarCollapsed } from '~/stores/ui';
+import loggerFactory from '~/utils/logger';
 
 
 import GrowiContextualSubNavigation from './GrowiContextualSubNavigation';
 import GrowiContextualSubNavigation from './GrowiContextualSubNavigation';
 
 
+import styles from './GrowiSubNavigationSwitcher.module.scss';
+
 const logger = loggerFactory('growi:cli:GrowiSubNavigationSticky');
 const logger = loggerFactory('growi:cli:GrowiSubNavigationSticky');
 
 
 
 
@@ -108,11 +110,13 @@ const GrowiSubNavigationSwitcher = (props) => {
 
 
   }, [initWidth, initVisible]);
   }, [initWidth, initVisible]);
 
 
+  // ${styles['grw-subnav-switcher']}
+
   return (
   return (
-    <div className={`grw-subnav-switcher ${isVisible ? '' : 'grw-subnav-switcher-hidden'}`}>
+    <div className={`${styles['grw-subnav-switcher']} ${isVisible ? '' : 'grw-subnav-switcher-hidden'}`}>
       <div
       <div
         id="grw-subnav-fixed-container"
         id="grw-subnav-fixed-container"
-        className="grw-subnav-fixed-container position-fixed grw-subnav-append-shadow-container"
+        className={`grw-subnav-fixed-container ${styles['grw-subnav-fixed-container']} position-fixed grw-subnav-append-shadow-container`}
         ref={fixedContainerRef}
         ref={fixedContainerRef}
         style={{ width }}
         style={{ width }}
       >
       >

+ 32 - 0
packages/app/src/components/Navbar/GrowiSubNavigationSwitcher.module.scss

@@ -0,0 +1,32 @@
+@use '~/styles/variables' as var;
+@use '~/styles/bootstrap/init' as bs;
+
+/*
+ * 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 {
+  :global {
+    .grw-subnav-fixed-container {
+      transition: transform 150ms $easeInOutCubic;
+    }
+  }
+
+  &:global {
+    &.grw-subnav-switcher-hidden {
+      .grw-subnav-fixed-container {
+        transition: unset;
+        transform: translateY(-100%);
+      }
+    }
+  }
+}

+ 18 - 5
packages/app/src/components/Navbar/SubNavButtons.tsx

@@ -1,5 +1,7 @@
 import React, { useCallback } from 'react';
 import React, { useCallback } from 'react';
 
 
+import dynamic from 'next/dynamic';
+
 import { toggleBookmark, toggleLike, toggleSubscribe } from '~/client/services/page-operation';
 import { toggleBookmark, toggleLike, toggleSubscribe } from '~/client/services/page-operation';
 import {
 import {
   IPageInfoAll, IPageToDeleteWithMeta, IPageToRenameWithMeta, isIPageInfoForEntity, isIPageInfoForOperation,
   IPageInfoAll, IPageToDeleteWithMeta, IPageToRenameWithMeta, isIPageInfoForEntity, isIPageInfoForOperation,
@@ -10,13 +12,10 @@ import { IPageForPageDuplicateModal } from '~/stores/modal';
 import { useSWRBookmarkInfo } from '../../stores/bookmark';
 import { useSWRBookmarkInfo } from '../../stores/bookmark';
 import { useSWRxPageInfo } from '../../stores/page';
 import { useSWRxPageInfo } from '../../stores/page';
 import { useSWRxUsersList } from '../../stores/user';
 import { useSWRxUsersList } from '../../stores/user';
-import BookmarkButtons from '../BookmarkButtons';
 import {
 import {
-  AdditionalMenuItemsRendererProps, ForceHideMenuItems, MenuItemType, PageItemControl,
+  AdditionalMenuItemsRendererProps, ForceHideMenuItems, MenuItemType, PageItemControlProps,
 } from '../Common/Dropdown/PageItemControl';
 } from '../Common/Dropdown/PageItemControl';
-import LikeButtons from '../LikeButtons';
-import SubscribeButton from '../SubscribeButton';
-import SeenUserInfo from '../User/SeenUserInfo';
+import { Skelton } from '../Skelton';
 
 
 
 
 type CommonProps = {
 type CommonProps = {
@@ -52,6 +51,20 @@ const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element
 
 
   const { data: bookmarkInfo, mutate: mutateBookmarkInfo } = useSWRBookmarkInfo(pageId);
   const { data: bookmarkInfo, mutate: mutateBookmarkInfo } = useSWRBookmarkInfo(pageId);
 
 
+  // dynamic import for show skelton
+  const SubscribeButton = dynamic(() => import('../SubscribeButton'), { ssr: false, loading: () => <Skelton width={37} additionalClass='btn-subscribe'/> });
+  const LikeButtons = dynamic(() => import('../LikeButtons'), { ssr: false, loading: () => <Skelton width={57.48} additionalClass='btn-like'/> });
+  const BookmarkButtons = dynamic(() => import('../BookmarkButtons'), {
+    ssr: false,
+    loading: () => <Skelton width={53.48} additionalClass='total-bookmarks'/>,
+  });
+  const SeenUserInfo = dynamic(() => import('../User/SeenUserInfo'), { ssr: false, loading: () => <Skelton width={58.98} additionalClass='btn-seen-user'/> });
+  const PageItemControl = dynamic<PageItemControlProps>(() => import('../Common/Dropdown/PageItemControl').then(mod => mod.PageItemControl), {
+    ssr: false,
+    loading: () => <Skelton width={37} additionalClass='btn-page-item-control'/>,
+  });
+
+
   const likerIds = isIPageInfoForEntity(pageInfo) ? (pageInfo.likerIds ?? []).slice(0, 15) : [];
   const likerIds = isIPageInfoForEntity(pageInfo) ? (pageInfo.likerIds ?? []).slice(0, 15) : [];
   const seenUserIds = isIPageInfoForEntity(pageInfo) ? (pageInfo.seenUserIds ?? []).slice(0, 15) : [];
   const seenUserIds = isIPageInfoForEntity(pageInfo) ? (pageInfo.seenUserIds ?? []).slice(0, 15) : [];
 
 

+ 12 - 10
packages/app/src/components/Page/DisplaySwitcher.tsx

@@ -23,6 +23,8 @@ import { Page } from '../Page';
 import TableOfContents from '../TableOfContents';
 import TableOfContents from '../TableOfContents';
 import UserInfo from '../User/UserInfo';
 import UserInfo from '../User/UserInfo';
 
 
+import styles from '../TableOfContents.module.scss';
+
 
 
 const WIKI_HEADER_LINK = 120;
 const WIKI_HEADER_LINK = 120;
 
 
@@ -62,7 +64,14 @@ const DisplaySwitcher = (): JSX.Element => {
     <>
     <>
       <TabContent activeTab={editorMode}>
       <TabContent activeTab={editorMode}>
         <TabPane tabId={EditorMode.View}>
         <TabPane tabId={EditorMode.View}>
-          <div className="d-flex flex-column flex-lg-row-reverse">
+          <div className="d-flex flex-column flex-lg-row">
+
+            <div className="flex-grow-1 flex-basis-0 mw-0">
+              { isUserPage && <UserInfo pageUser={pageUser} />}
+              { !isNotFound && <Page /> }
+              { isNotFound && !isNotCreatable && <NotFoundPage /> }
+              { isNotFound && isNotCreatable && <NotCreatablePage /> }
+            </div>
 
 
             { !isNotFound && !currentPage?.isEmpty && (
             { !isNotFound && !currentPage?.isEmpty && (
               <div className="grw-side-contents-container">
               <div className="grw-side-contents-container">
@@ -103,8 +112,8 @@ const DisplaySwitcher = (): JSX.Element => {
                   ) }
                   ) }
 
 
                   <div className="d-none d-lg-block">
                   <div className="d-none d-lg-block">
-                    <div id="revision-toc" className="revision-toc">
-                      {/* <TableOfContents /> */}
+                    <div id="revision-toc" className={`revision-toc ${styles['revision-toc']}`}>
+                      <TableOfContents />
                     </div>
                     </div>
                     <ContentLinkButtons />
                     <ContentLinkButtons />
                   </div>
                   </div>
@@ -113,13 +122,6 @@ const DisplaySwitcher = (): JSX.Element => {
               </div>
               </div>
             ) }
             ) }
 
 
-            <div className="flex-grow-1 flex-basis-0 mw-0">
-              { isUserPage && <UserInfo pageUser={pageUser} />}
-              { !isNotFound && <Page /> }
-              { isNotFound && !isNotCreatable && <NotFoundPage /> }
-              { isNotFound && isNotCreatable && <NotCreatablePage /> }
-            </div>
-
           </div>
           </div>
         </TabPane>
         </TabPane>
         { isEditable && (
         { isEditable && (

+ 6 - 2
packages/app/src/components/PagePathNav.tsx

@@ -1,10 +1,12 @@
 import React, { FC } from 'react';
 import React, { FC } from 'react';
+
 import { DevidedPagePath } from '@growi/core';
 import { DevidedPagePath } from '@growi/core';
-import PagePathHierarchicalLink from './PagePathHierarchicalLink';
-import CopyDropdown from './Page/CopyDropdown';
+import dynamic from 'next/dynamic';
 
 
 import LinkedPagePath from '../models/linked-page-path';
 import LinkedPagePath from '../models/linked-page-path';
 
 
+import PagePathHierarchicalLink from './PagePathHierarchicalLink';
+
 
 
 type Props = {
 type Props = {
   pagePath: string,
   pagePath: string,
@@ -19,6 +21,8 @@ const PagePathNav: FC<Props> = (props: Props) => {
   } = props;
   } = props;
   const dPagePath = new DevidedPagePath(pagePath, false, true);
   const dPagePath = new DevidedPagePath(pagePath, false, true);
 
 
+  const CopyDropdown = dynamic(() => import('./Page/CopyDropdown'), { ssr: false });
+
   let formerLink;
   let formerLink;
   let latterLink;
   let latterLink;
 
 

+ 9 - 12
packages/app/src/components/Sidebar/CustomSidebar.tsx

@@ -1,13 +1,14 @@
 import React, { FC } from 'react';
 import React, { FC } from 'react';
 
 
-import AppContainer from '~/client/services/AppContainer';
-import loggerFactory from '~/utils/logger';
-import { useSWRxPageByPath } from '~/stores/page';
+import { useTranslation } from 'next-i18next';
 
 
-import { withUnstatedContainers } from '../UnstatedUtils';
-import RevisionRenderer from '../Page/RevisionRenderer';
 import { IRevision } from '~/interfaces/revision';
 import { IRevision } from '~/interfaces/revision';
+import { useSWRxPageByPath } from '~/stores/page';
 import { useCustomSidebarOptions } from '~/stores/renderer';
 import { useCustomSidebarOptions } from '~/stores/renderer';
+import loggerFactory from '~/utils/logger';
+
+import RevisionRenderer from '../Page/RevisionRenderer';
+
 
 
 const logger = loggerFactory('growi:cli:CustomSidebar');
 const logger = loggerFactory('growi:cli:CustomSidebar');
 
 
@@ -22,12 +23,8 @@ const SidebarNotFound = () => {
   );
   );
 };
 };
 
 
-type Props = {
-  appContainer: AppContainer,
-};
-
-const CustomSidebar: FC<Props> = (props: Props) => {
-
+const CustomSidebar: FC = () => {
+  const { t } = useTranslation();
   const { data: rendererOptions } = useCustomSidebarOptions();
   const { data: rendererOptions } = useCustomSidebarOptions();
 
 
   const { data: page, error, mutate } = useSWRxPageByPath('/Sidebar');
   const { data: page, error, mutate } = useSWRxPageByPath('/Sidebar');
@@ -43,7 +40,7 @@ const CustomSidebar: FC<Props> = (props: Props) => {
     <>
     <>
       <div className="grw-sidebar-content-header p-3 d-flex">
       <div className="grw-sidebar-content-header p-3 d-flex">
         <h3 className="mb-0">
         <h3 className="mb-0">
-          Custom Sidebar
+          {t('CustomSidebar')}
           <a className="h6 ml-2" href="/Sidebar"><i className="icon-pencil"></i></a>
           <a className="h6 ml-2" href="/Sidebar"><i className="icon-pencil"></i></a>
         </h3>
         </h3>
         <button type="button" className="btn btn-sm ml-auto grw-btn-reload" onClick={() => mutate()}>
         <button type="button" className="btn btn-sm ml-auto grw-btn-reload" onClick={() => mutate()}>

+ 2 - 5
packages/app/src/components/Sidebar/SidebarContents.tsx

@@ -3,13 +3,11 @@ import React from 'react';
 import { SidebarContentsType } from '~/interfaces/ui';
 import { SidebarContentsType } from '~/interfaces/ui';
 import { useCurrentSidebarContents } from '~/stores/ui';
 import { useCurrentSidebarContents } from '~/stores/ui';
 
 
-// import CustomSidebar from './CustomSidebar';
+import CustomSidebar from './CustomSidebar';
 import PageTree from './PageTree';
 import PageTree from './PageTree';
 import RecentChanges from './RecentChanges';
 import RecentChanges from './RecentChanges';
 import Tag from './Tag';
 import Tag from './Tag';
 
 
-const DummyComponent = (): JSX.Element => <></>; // Todo: remove this later when it is able to render other Contents.
-
 export const SidebarContents = (): JSX.Element => {
 export const SidebarContents = (): JSX.Element => {
   const { data: currentSidebarContents } = useCurrentSidebarContents();
   const { data: currentSidebarContents } = useCurrentSidebarContents();
 
 
@@ -19,8 +17,7 @@ export const SidebarContents = (): JSX.Element => {
       Contents = RecentChanges;
       Contents = RecentChanges;
       break;
       break;
     case SidebarContentsType.CUSTOM:
     case SidebarContentsType.CUSTOM:
-      // Contents = CustomSidebar;
-      Contents = DummyComponent;
+      Contents = CustomSidebar;
       break;
       break;
     case SidebarContentsType.TAG:
     case SidebarContentsType.TAG:
       Contents = Tag;
       Contents = Tag;

+ 21 - 0
packages/app/src/components/Skelton.tsx

@@ -0,0 +1,21 @@
+import React from 'react';
+
+type SkeltonProps = {
+  width?: number,
+  height?: number,
+  additionalClass?: string,
+  roundedPill?: boolean,
+}
+
+export const Skelton = (props: SkeltonProps): JSX.Element => {
+  const {
+    width, height, additionalClass, roundedPill,
+  } = props;
+
+  const style = {
+    width,
+    height,
+  };
+
+  return <div style={style} className={`grw-skelton ${additionalClass} ${roundedPill ? 'rounded-pill' : ''}`}></div>;
+};

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

@@ -36,7 +36,7 @@ const SubscribeButton: FC<Props> = (props: Props) => {
         type="button"
         type="button"
         id="subscribe-button"
         id="subscribe-button"
         onClick={props.onClick}
         onClick={props.onClick}
-        className={`shadow-none btn ${styles['btn-subscribe']} border-0
+        className={`shadow-none btn btn-subscribe ${styles['btn-subscribe']} border-0
           ${isSubscribing ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`}
           ${isSubscribing ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`}
       >
       >
         <i className={`fa ${isSubscribing ? 'fa-bell' : 'fa-bell-slash-o'}`}></i>
         <i className={`fa ${isSubscribing ? 'fa-bell' : 'fa-bell-slash-o'}`}></i>

+ 13 - 15
packages/app/src/components/SuspenseUtils.jsx

@@ -5,19 +5,17 @@ import React, { Suspense } from 'react';
  * If you throw a Promise in the component, it will display a sppiner
  * If you throw a Promise in the component, it will display a sppiner
  * @param {object} Component A React.Component or functional component
  * @param {object} Component A React.Component or functional component
  */
  */
-export function withLoadingSppiner(Component) {
-  return (props => function getWithLoadingSpinner() {
-    return (
+export const withLoadingSppiner = Component => function getWithLoadingSpinner(props) {
+  return (
     // wrap with <Suspense></Suspense>
     // wrap with <Suspense></Suspense>
-      <Suspense
-        fallback={(
-          <div className="my-5 text-center">
-            <i className="fa fa-lg fa-spinner fa-pulse mx-auto text-muted"></i>
-          </div>
-        )}
-      >
-        <Component {...props} />
-      </Suspense>
-    );
-  });
-}
+    <Suspense
+      fallback={(
+        <div className="my-5 text-center">
+          <i className="fa fa-lg fa-spinner fa-pulse mx-auto text-muted"></i>
+        </div>
+      )}
+    >
+      <Component {...props} />
+    </Suspense>
+  );
+};

+ 28 - 0
packages/app/src/components/TableOfContents.module.scss

@@ -0,0 +1,28 @@
+
+.revision-toc :global {
+  // to get on the Attachment row
+  z-index: 1;
+  padding: 5px;
+  font-size: 0.9em;
+
+  border-bottom: 1px solid transparent;
+
+  .revision-toc-content {
+    li {
+      margin: 6px;
+    }
+    > ul {
+      padding-left: 0;
+      ul {
+        padding-left: 1em;
+      }
+    }
+
+    // first level of li
+    > ul > li {
+      padding: 5px;
+      margin-right: 4px;
+      margin-left: 17px;
+    }
+  }
+}

+ 22 - 52
packages/app/src/components/TableOfContents.jsx → packages/app/src/components/TableOfContents.tsx

@@ -1,37 +1,35 @@
 import React, { useCallback, useEffect, useState } from 'react';
 import React, { useCallback, useEffect, useState } from 'react';
 
 
-import PropTypes from 'prop-types';
+import ReactMarkdown from 'react-markdown';
 
 
-
-import PageContainer from '~/client/services/PageContainer';
 import { blinkElem } from '~/client/util/blink-section-header';
 import { blinkElem } from '~/client/util/blink-section-header';
 import { addSmoothScrollEvent } from '~/client/util/smooth-scroll';
 import { addSmoothScrollEvent } from '~/client/util/smooth-scroll';
+import { useIsUserPage } from '~/stores/context';
+import { useTocOptions } from '~/stores/renderer';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 
 
 import { StickyStretchableScroller } from './StickyStretchableScroller';
 import { StickyStretchableScroller } from './StickyStretchableScroller';
-import { withUnstatedContainers } from './UnstatedUtils';
 
 
 // eslint-disable-next-line no-unused-vars
 // eslint-disable-next-line no-unused-vars
 const logger = loggerFactory('growi:TableOfContents');
 const logger = loggerFactory('growi:TableOfContents');
 
 
-/**
- * @author Yuki Takei <yuki@weseek.co.jp>
- *
- */
-const TableOfContents = (props) => {
+const TableOfContents = (): JSX.Element => {
 
 
-  const { pageContainer } = props;
-  const { pageUser } = pageContainer.state;
-  const isUserPage = pageUser != null;
+  const { data: isUserPage } = useIsUserPage();
 
 
   const [tocHtml, setTocHtml] = useState('');
   const [tocHtml, setTocHtml] = useState('');
 
 
+  const { data: rendererOptions } = useTocOptions();
+
   const calcViewHeight = useCallback(() => {
   const calcViewHeight = useCallback(() => {
     // calculate absolute top of '#revision-toc' element
     // calculate absolute top of '#revision-toc' element
     const parentElem = document.querySelector('.grw-side-contents-container');
     const parentElem = document.querySelector('.grw-side-contents-container');
-    const parentBottom = parentElem.getBoundingClientRect().bottom;
     const containerElem = document.querySelector('#revision-toc');
     const containerElem = document.querySelector('#revision-toc');
+    if (parentElem == null || containerElem == null) {
+      return 0;
+    }
+    const parentBottom = parentElem.getBoundingClientRect().bottom;
     const containerTop = containerElem.getBoundingClientRect().top;
     const containerTop = containerElem.getBoundingClientRect().top;
     const containerComputedStyle = getComputedStyle(containerElem);
     const containerComputedStyle = getComputedStyle(containerElem);
     const containerPaddingTop = parseFloat(containerComputedStyle['padding-top']);
     const containerPaddingTop = parseFloat(containerComputedStyle['padding-top']);
@@ -49,56 +47,28 @@ const TableOfContents = (props) => {
 
 
   useEffect(() => {
   useEffect(() => {
     const tocDom = document.getElementById('revision-toc-content');
     const tocDom = document.getElementById('revision-toc-content');
+    if (tocDom == null) { return }
     const anchorsInToc = Array.from(tocDom.getElementsByTagName('a'));
     const anchorsInToc = Array.from(tocDom.getElementsByTagName('a'));
     addSmoothScrollEvent(anchorsInToc, blinkElem);
     addSmoothScrollEvent(anchorsInToc, blinkElem);
   }, [tocHtml]);
   }, [tocHtml]);
 
 
-  // == TODO: render ToC without globalEmitter -- Yuki Takei
-  //
-  // set handler to render ToC
-  // useEffect(() => {
-  //   const handler = html => setTocHtml(html);
-  //   globalEmitter.on('renderTocHtml', handler);
-
-  //   return function cleanup() {
-  //     globalEmitter.removeListener('renderTocHtml', handler);
-  //   };
-  // }, [globalEmitter]);
-
   return (
   return (
     <StickyStretchableScroller
     <StickyStretchableScroller
       stickyElemSelector=".grw-side-contents-sticky-container"
       stickyElemSelector=".grw-side-contents-sticky-container"
       calcViewHeight={calcViewHeight}
       calcViewHeight={calcViewHeight}
     >
     >
-      { tocHtml !== ''
-        ? (
-          <div
-            id="revision-toc-content"
-            className="revision-toc-content mb-3"
-            // eslint-disable-next-line react/no-danger
-            dangerouslySetInnerHTML={{ __html: tocHtml }}
-          />
-        )
-        : (
-          <div
-            id="revision-toc-content"
-            className="revision-toc-content mb-2"
-          >
-          </div>
-        ) }
-
+      <div
+        id="revision-toc-content"
+        className="revision-toc-content mb-3"
+      >
+        {/* parse blank to show toc (https://github.com/weseek/growi/pull/6277) */}
+        <ReactMarkdown {...rendererOptions}>
+          {''}
+        </ReactMarkdown>
+      </div>
     </StickyStretchableScroller>
     </StickyStretchableScroller>
   );
   );
 
 
 };
 };
 
 
-/**
- * Wrapper component for using unstated
- */
-const TableOfContentsWrapper = withUnstatedContainers(TableOfContents, [PageContainer]);
-
-TableOfContents.propTypes = {
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
-};
-
-export default TableOfContentsWrapper;
+export default TableOfContents;

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

@@ -137,6 +137,7 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
   const router = useRouter();
   const router = useRouter();
 
 
   const UnsavedAlertDialog = dynamic(() => import('./UnsavedAlertDialog'), { ssr: false });
   const UnsavedAlertDialog = dynamic(() => import('./UnsavedAlertDialog'), { ssr: false });
+  const GrowiSubNavigationSwitcher = dynamic(() => import('../components/Navbar/GrowiSubNavigationSwitcher'), { ssr: false });
 
 
   const { data: currentUser } = useCurrentUser(props.currentUser != null ? JSON.parse(props.currentUser) : null);
   const { data: currentUser } = useCurrentUser(props.currentUser != null ? JSON.parse(props.currentUser) : null);
 
 
@@ -254,8 +255,7 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
           <GrowiContextualSubNavigation isLinkSharingDisabled={props.disableLinkSharing} />
           <GrowiContextualSubNavigation isLinkSharingDisabled={props.disableLinkSharing} />
         </header>
         </header>
         <div className="d-edit-none">
         <div className="d-edit-none">
-          {/* <GrowiSubNavigationSwitcher /> */}
-          GrowiSubNavigationSwitcher
+          <GrowiSubNavigationSwitcher />
         </div>
         </div>
 
 
         <div id="grw-subnav-sticky-trigger" className="sticky-top"></div>
         <div id="grw-subnav-sticky-trigger" className="sticky-top"></div>

+ 15 - 4
packages/app/src/pages/admin/[[...path]].page.tsx

@@ -1,3 +1,5 @@
+import React from 'react';
+
 import {
 import {
   NextPage, GetServerSideProps, GetServerSidePropsContext,
   NextPage, GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
 } from 'next';
@@ -9,9 +11,11 @@ import AdminHome from '~/components/Admin/AdminHome/AdminHome';
 import AppSettingsPageContents from '~/components/Admin/App/AppSettingsPageContents';
 import AppSettingsPageContents from '~/components/Admin/App/AppSettingsPageContents';
 import ExportArchiveDataPage from '~/components/Admin/ExportArchiveDataPage';
 import ExportArchiveDataPage from '~/components/Admin/ExportArchiveDataPage';
 import DataImportPageContents from '~/components/Admin/ImportData/ImportDataPageContents';
 import DataImportPageContents from '~/components/Admin/ImportData/ImportDataPageContents';
+import LegacySlackIntegration from '~/components/Admin/LegacySlackIntegration/LegacySlackIntegration';
 import MarkDownSettingContents from '~/components/Admin/MarkdownSetting/MarkDownSettingContents';
 import MarkDownSettingContents from '~/components/Admin/MarkdownSetting/MarkDownSettingContents';
 import NotificationSetting from '~/components/Admin/Notification/NotificationSetting';
 import NotificationSetting from '~/components/Admin/Notification/NotificationSetting';
 import SecurityManagementContents from '~/components/Admin/Security/SecurityManagementContents';
 import SecurityManagementContents from '~/components/Admin/Security/SecurityManagementContents';
+import SlackIntegration from '~/components/Admin/SlackIntegration/SlackIntegration';
 import UserGroupPage from '~/components/Admin/UserGroup/UserGroupPage';
 import UserGroupPage from '~/components/Admin/UserGroup/UserGroupPage';
 import UserManagement from '~/components/Admin/UserManagement';
 import UserManagement from '~/components/Admin/UserManagement';
 import AdminLayout from '~/components/Layout/AdminLayout';
 import AdminLayout from '~/components/Layout/AdminLayout';
@@ -52,7 +56,7 @@ const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
   const path = router.query.path || 'home';
   const path = router.query.path || 'home';
   const name = Array.isArray(path) ? path[0] : path;
   const name = Array.isArray(path) ? path[0] : path;
 
 
-  // const CustomizeSettingContents = dynamic(() => import('../../components/Admin/Customize/Customize'), { ssr: false });
+  const CustomizeSettingContents = dynamic(() => import('../../components/Admin/Customize/Customize'), { ssr: false });
 
 
   const adminPagesMap = {
   const adminPagesMap = {
     home: {
     home: {
@@ -78,8 +82,7 @@ const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
     },
     },
     customize: {
     customize: {
       title: useCustomTitle(props, t('Customize Settings')),
       title: useCustomTitle(props, t('Customize Settings')),
-      // component: <CustomizeSettingContents />,
-      component: <>CustomizeSettingContents</>,
+      component: <CustomizeSettingContents />,
     },
     },
     importer: {
     importer: {
       title: useCustomTitle(props, t('Import Data')),
       title: useCustomTitle(props, t('Import Data')),
@@ -98,13 +101,21 @@ const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
       title: '',
       title: '',
       component: <>global-notification</>,
       component: <>global-notification</>,
     },
     },
+    'slack-integration': {
+      title: useCustomTitle(props, t('slack_integration')),
+      component: <SlackIntegration />,
+    },
+    'slack-integration-legacy': {
+      title: useCustomTitle(props, t('Legacy_Slack_Integration')),
+      component: <LegacySlackIntegration />,
+    },
     users: {
     users: {
       title: useCustomTitle(props, t('User_Management')),
       title: useCustomTitle(props, t('User_Management')),
       component: <UserManagement />,
       component: <UserManagement />,
     },
     },
     'user-groups': {
     'user-groups': {
       title: useCustomTitle(props, t('UserGroup Management')),
       title: useCustomTitle(props, t('UserGroup Management')),
-      component: <>user-groups</>,
+      component: <UserGroupPage />,
     },
     },
     search: {
     search: {
       title: useCustomTitle(props, t('Full Text Search Management')),
       title: useCustomTitle(props, t('Full Text Search Management')),

+ 89 - 0
packages/app/src/pages/installer.page.tsx

@@ -0,0 +1,89 @@
+import React from 'react';
+
+import { pagePathUtils } from '@growi/core';
+import {
+  NextPage, GetServerSideProps, GetServerSidePropsContext,
+} from 'next';
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
+
+import { RawLayout } from '~/components/Layout/RawLayout';
+
+import InstallerForm from '../components/InstallerForm';
+import {
+  useCurrentPagePath, useCsrfToken,
+  useAppTitle, useSiteUrl, useConfidential,
+} from '../stores/context';
+
+
+import {
+  CommonProps, getNextI18NextConfig, getServerSideCommonProps, useCustomTitle,
+} from './commons';
+
+
+const { isTrashPage: _isTrashPage } = pagePathUtils;
+
+async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
+  const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired);
+  props._nextI18Next = nextI18NextConfig._nextI18Next;
+}
+
+type Props = CommonProps & {
+
+  pageWithMetaStr: string,
+};
+
+const InstallerPage: NextPage<Props> = (props: Props) => {
+
+  // commons
+  useAppTitle(props.appTitle);
+  useSiteUrl(props.siteUrl);
+  useConfidential(props.confidential);
+  useCsrfToken(props.csrfToken);
+
+  // page
+  useCurrentPagePath(props.currentPathname);
+
+  const classNames: string[] = [];
+
+  return (
+    <>
+      <RawLayout title={useCustomTitle(props, 'GROWI')} className={classNames.join(' ')}>
+        <div id="page-wrapper">
+          <div className="main container-fluid">
+
+            <div className="row">
+              <div className="col-md-12">
+                <div className="login-header mx-auto">
+                  <h1 className="my-3">GROWI</h1>
+                </div>
+              </div>
+              <div className="col-md-12">
+                <InstallerForm />
+              </div>
+            </div>
+          </div>
+        </div>
+      </RawLayout>
+    </>
+  );
+};
+
+export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+  const result = await getServerSideCommonProps(context);
+
+  // check for presence
+  // see: https://github.com/vercel/next.js/issues/19271#issuecomment-730006862
+  if (!('props' in result)) {
+    throw new Error('invalid getSSP result');
+  }
+
+  const props: Props = result.props as Props;
+
+  injectNextI18NextConfigurations(context, props, ['translation']);
+
+  return {
+    props,
+  };
+};
+
+export default InstallerPage;

+ 1 - 1
packages/app/src/server/routes/index.js

@@ -94,7 +94,7 @@ module.exports = function(crowi, app) {
   // installer
   // installer
   if (!isInstalled) {
   if (!isInstalled) {
     const installer = require('./installer')(crowi);
     const installer = require('./installer')(crowi);
-    app.get('/installer'              , applicationNotInstalled , installer.index);
+    app.get('/installer'              , applicationNotInstalled, next.delegateToNext);
     app.post('/installer'             , applicationNotInstalled , registerFormValidator.registerRules(), registerFormValidator.registerValidation, csrfProtection, addActivity, installer.install);
     app.post('/installer'             , applicationNotInstalled , registerFormValidator.registerRules(), registerFormValidator.registerValidation, csrfProtection, addActivity, installer.install);
     return;
     return;
   }
   }

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

@@ -1,6 +1,8 @@
 import { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
 import { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
+import raw from 'rehype-raw';
+import sanitize, { defaultSchema } from 'rehype-sanitize';
 import slug from 'rehype-slug';
 import slug from 'rehype-slug';
-// import toc, { HtmlElementNode } from 'rehype-toc';
+import toc, { HtmlElementNode } from 'rehype-toc';
 import breaks from 'remark-breaks';
 import breaks from 'remark-breaks';
 import emoji from 'remark-emoji';
 import emoji from 'remark-emoji';
 import footnotes from 'remark-footnotes';
 import footnotes from 'remark-footnotes';
@@ -214,14 +216,27 @@ export interface ReactMarkdownOptionsGenerator {
 const generateCommonOptions: ReactMarkdownOptionsGenerator = (config: RendererConfig): RendererOptions => {
 const generateCommonOptions: ReactMarkdownOptionsGenerator = (config: RendererConfig): RendererOptions => {
   return {
   return {
     remarkPlugins: [gfm],
     remarkPlugins: [gfm],
-    rehypePlugins: [slug],
+    rehypePlugins: [
+      slug,
+      raw,
+      [sanitize, {
+        ...defaultSchema,
+        attributes: {
+          ...defaultSchema.attributes,
+          '*': ['className', 'class'],
+        },
+      }],
+    ],
     components: {
     components: {
       a: NextLink,
       a: NextLink,
     },
     },
   };
   };
 };
 };
 
 
-export const generateViewOptions: ReactMarkdownOptionsGenerator = (config: RendererConfig): RendererOptions => {
+export const generateViewOptions = (
+    config: RendererConfig,
+    storeTocNode: (node: HtmlElementNode) => void,
+): RendererOptions => {
 
 
   const options = generateCommonOptions(config);
   const options = generateCommonOptions(config);
 
 
@@ -236,11 +251,29 @@ export const generateViewOptions: ReactMarkdownOptionsGenerator = (config: Rende
     }
     }
   }
   }
 
 
-  // add rehypePlugins
-  // rehypePlugins.push([toc, {
-  //   headings: ['h1', 'h2', 'h3'],
-  //   customizeTOC: storeTocNode,
-  // }]);
+  // store toc node
+  if (rehypePlugins != null) {
+    rehypePlugins.push([toc, {
+      nav: false,
+      headings: ['h1', 'h2', 'h3'],
+      customizeTOC: (toc: HtmlElementNode) => {
+        // method for replace <ol> to <ul>
+        const replacer = (children) => {
+          children.forEach((child) => {
+            if (child.type === 'element' && child.tagName === 'ol') {
+              child.tagName = 'ul';
+            }
+            if (child.children) {
+              replacer(child.children);
+            }
+          });
+        };
+        replacer([toc]); // replace <ol> to <ul>
+        storeTocNode(toc); // store tocNode to global state with swr
+        return false; // not show toc in body
+      },
+    }]);
+  }
   // renderer.rehypePlugins.push([autoLinkHeadings, {
   // renderer.rehypePlugins.push([autoLinkHeadings, {
   //   behavior: 'append',
   //   behavior: 'append',
   // }]);
   // }]);
@@ -267,6 +300,30 @@ export const generateViewOptions: ReactMarkdownOptionsGenerator = (config: Rende
   return options;
   return options;
 };
 };
 
 
+export const generateTocOptions = (config: RendererConfig, tocNode: HtmlElementNode | undefined): RendererOptions => {
+
+  const options = generateCommonOptions(config);
+
+  const { remarkPlugins, rehypePlugins } = options;
+
+  // add remark plugins
+  if (remarkPlugins != null) {
+    remarkPlugins.push(emoji);
+  }
+  // set toc node
+  if (rehypePlugins != null) {
+    rehypePlugins.push([toc, {
+      headings: ['h1', 'h2', 'h3'],
+      customizeTOC: () => tocNode,
+    }]);
+  }
+  // renderer.rehypePlugins.push([autoLinkHeadings, {
+  //   behavior: 'append',
+  // }]);
+
+  return options;
+};
+
 export const generatePreviewOptions: ReactMarkdownOptionsGenerator = (config: RendererConfig): RendererOptions => {
 export const generatePreviewOptions: ReactMarkdownOptionsGenerator = (config: RendererConfig): RendererOptions => {
   const options = generateCommonOptions(config);
   const options = generateCommonOptions(config);
 
 

+ 5 - 0
packages/app/src/stores/context.tsx

@@ -1,5 +1,6 @@
 import EventEmitter from 'events';
 import EventEmitter from 'events';
 
 
+import { HtmlElementNode } from 'rehype-toc';
 import { Key, SWRResponse } from 'swr';
 import { Key, SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 import useSWRImmutable from 'swr/immutable';
 
 
@@ -223,6 +224,10 @@ export const useRendererConfig = (initialData?: RendererConfig): SWRResponse<Ren
   return useStaticSWR('growiRendererConfig', initialData);
   return useStaticSWR('growiRendererConfig', initialData);
 };
 };
 
 
+export const useCurrentPageTocNode = (): SWRResponse<HtmlElementNode, any> => {
+  return useStaticSWR('currentPageTocNode');
+};
+
 export const useIsBlinkedHeaderAtBoot = (initialData?: boolean): SWRResponse<boolean, Error> => {
 export const useIsBlinkedHeaderAtBoot = (initialData?: boolean): SWRResponse<boolean, Error> => {
   return useStaticSWR('isBlinkedAtBoot', initialData);
   return useStaticSWR('isBlinkedAtBoot', initialData);
 };
 };

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

@@ -3,13 +3,17 @@ import useSWRImmutable from 'swr/immutable';
 
 
 import {
 import {
   ReactMarkdownOptionsGenerator, RendererOptions,
   ReactMarkdownOptionsGenerator, RendererOptions,
-  generateViewOptions, generatePreviewOptions, generateCommentPreviewOptions, generateOthersOptions,
+  generatePreviewOptions, generateCommentPreviewOptions, generateOthersOptions,
+  generateViewOptions, generateTocOptions,
 } from '~/services/renderer/renderer';
 } from '~/services/renderer/renderer';
 
 
-import { useRendererConfig } from './context';
+
+import { useCurrentPageTocNode, useRendererConfig } from './context';
 
 
 // The base hook with common processes
 // The base hook with common processes
-const _useOptionsBase = (rendererId: string, generator: ReactMarkdownOptionsGenerator): SWRResponse<RendererOptions, Error> => {
+const _useOptionsBase = (
+    rendererId: string, generator: ReactMarkdownOptionsGenerator,
+): SWRResponse<RendererOptions, Error> => {
   const { data: rendererConfig } = useRendererConfig();
   const { data: rendererConfig } = useRendererConfig();
 
 
   const isAllDataValid = rendererConfig != null;
   const isAllDataValid = rendererConfig != null;
@@ -31,7 +35,17 @@ const _useOptionsBase = (rendererId: string, generator: ReactMarkdownOptionsGene
 export const useViewOptions = (): SWRResponse<RendererOptions, Error> => {
 export const useViewOptions = (): SWRResponse<RendererOptions, Error> => {
   const key = 'viewOptions';
   const key = 'viewOptions';
 
 
-  return _useOptionsBase(key, generateViewOptions);
+  const { mutate: storeTocNode } = useCurrentPageTocNode();
+
+  return _useOptionsBase(key, config => generateViewOptions(config, storeTocNode));
+};
+
+export const useTocOptions = (): SWRResponse<RendererOptions, Error> => {
+  const key = 'tocOptions';
+
+  const { data: tocNode } = useCurrentPageTocNode();
+
+  return _useOptionsBase(key, config => generateTocOptions(config, tocNode));
 };
 };
 
 
 export const usePreviewOptions = (): SWRResponse<RendererOptions, Error> => {
 export const usePreviewOptions = (): SWRResponse<RendererOptions, Error> => {

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

@@ -223,7 +223,7 @@ type PreferDrawerModeByUserUtils = {
   update: (preferDrawerMode: boolean) => void
   update: (preferDrawerMode: boolean) => void
 }
 }
 
 
-export const usePreferDrawerModeByUser = (initialData?: boolean): SWRResponseWithUtils<SWRResponse, PreferDrawerModeByUserUtils> => {
+export const usePreferDrawerModeByUser = (initialData?: boolean): SWRResponseWithUtils<PreferDrawerModeByUserUtils, boolean> => {
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
   const { scheduleToPut } = useUserUISettings();
   const { scheduleToPut } = useUserUISettings();
 
 

+ 0 - 4
packages/app/src/styles/_layout.scss

@@ -1,10 +1,6 @@
 @use './variables' as var;
 @use './variables' as var;
 @use './bootstrap/init' as bs;
 @use './bootstrap/init' as bs;
 
 
-:root {
-  font-size: var.$font-size-root;
-}
-
 body {
 body {
   overflow-y: scroll !important;
   overflow-y: scroll !important;
   overscroll-behavior-y: none;
   overscroll-behavior-y: none;

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

@@ -1,5 +1,3 @@
-$font-size-root: 14px;
-
 //== GROWI Official Color
 //== GROWI Official Color
 $growi-green: #74bc46;
 $growi-green: #74bc46;
 $growi-blue: #175fa5;
 $growi-blue: #175fa5;

+ 1 - 1
packages/app/src/styles/atoms/_buttons.scss

@@ -39,7 +39,7 @@
 }
 }
 
 
 // fill button style
 // fill button style
-:root .btn.btn-fill {
+.btn.btn-fill {
   position: relative;
   position: relative;
   display: flex;
   display: flex;
   justify-content: space-between;
   justify-content: space-between;

+ 4 - 7
packages/app/src/styles/bootstrap/_apply.scss

@@ -3,10 +3,7 @@
 @import '~bootstrap/scss/utilities';
 @import '~bootstrap/scss/utilities';
 @import '~bootstrap/scss/root';
 @import '~bootstrap/scss/root';
 
 
-// increase specificity with ':root' for GROWI theming
-:root {
-  // import bootstrap
-  @import '~bootstrap/scss/bootstrap';
-  // override
-  @import './override';
-}
+// import bootstrap
+@import '~bootstrap/scss/bootstrap';
+// override
+@import './override';

+ 4 - 0
packages/app/src/styles/bootstrap/_init.scss

@@ -2,4 +2,8 @@
 
 
 @import '~bootstrap/scss/functions';
 @import '~bootstrap/scss/functions';
 @import '~bootstrap/scss/variables';
 @import '~bootstrap/scss/variables';
+
+// merge $colors to $theme-colors
+$theme-colors: map-merge($theme-colors, $colors);
+
 @import '~bootstrap/scss/mixins';
 @import '~bootstrap/scss/mixins';

+ 1 - 0
packages/app/src/styles/bootstrap/_variables.scss

@@ -70,6 +70,7 @@ $font-family-serif: Georgia, 'Times New Roman', Times, serif;
 $font-family-monospace: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
 $font-family-monospace: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
 $font-family-base: $font-family-sans-serif;
 $font-family-base: $font-family-sans-serif;
 
 
+$font-size-base: 0.875rem;  // 16px -> 14px
 $line-height-base: 1.42857;
 $line-height-base: 1.42857;
 
 
 $blockquote-small-color: $gray-500;
 $blockquote-small-color: $gray-500;

+ 5 - 2
packages/app/src/styles/style-next.scss

@@ -6,8 +6,11 @@
 @import '~react-bootstrap-typeahead/css/Typeahead';
 @import '~react-bootstrap-typeahead/css/Typeahead';
 @import 'override-rbt';
 @import 'override-rbt';
 
 
-// // override simplebar-react styles
-// @import 'override-simplebar';
+// import SimpleBar styles
+@import '~simplebar/dist/simplebar.min.css';
+
+// override simplebar-react styles
+@import 'override-simplebar';
 
 
 // icons
 // icons
 @import '~simple-line-icons';
 @import '~simple-line-icons';

+ 7 - 0
packages/app/src/styles/theme/_apply-colors-dark.scss

@@ -513,3 +513,10 @@ ul.pagination {
 .grw-modal-head {
 .grw-modal-head {
   border-color: $border-color-global;
   border-color: $border-color-global;
 }
 }
+
+/*
+ * skelton
+ */
+.grw-skelton {
+  background-color: lighten($bgcolor-subnav, 20%);
+}

+ 7 - 0
packages/app/src/styles/theme/_apply-colors-light.scss

@@ -405,3 +405,10 @@ $dropdown-link-active-bg: $bgcolor-dropdown-link-active;
 .grw-modal-head {
 .grw-modal-head {
   border-color: $border-color-global;
   border-color: $border-color-global;
 }
 }
+
+/*
+ * skelton
+ */
+.grw-skelton {
+  background-color: $gray-200;
+}

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

@@ -65,15 +65,15 @@ pre:not(.hljs):not(.CodeMirror-line) {
 //== Apply to Bootstrap Elements
 //== Apply to Bootstrap Elements
 //
 //
 
 
-// // Alert link
-// @each $color, $value in $theme-colors {
-//   .alert.alert-#{$color} {
-//     a,
-//     a:hover {
-//       color: theme-color-level($color, $alert-color-level - 2);
-//     }
-//   }
-// }
+// Alert link
+@each $color, $value in $theme-colors {
+  .alert.alert-#{$color} {
+    a,
+    a:hover {
+      color: theme-color-level($color, $alert-color-level - 2);
+    }
+  }
+}
 
 
 // Dropdown
 // Dropdown
 .grw-apperance-mode-dropdown {
 .grw-apperance-mode-dropdown {

+ 1 - 7
packages/app/src/styles/theme/_reboot-bootstrap-colors.scss

@@ -14,14 +14,8 @@
 // 3. Set an explicit initial text-align value so that we can later use
 // 3. Set an explicit initial text-align value so that we can later use
 //    the `inherit` value on things like `<th>` elements.
 //    the `inherit` value on things like `<th>` elements.
 
 
-body {
-  // margin: 0; // 1
-  // font-family: $font-family-base;
-  // @include font-size($font-size-base);
-  // font-weight: $font-weight-base;
-  // line-height: $line-height-base;
+& {
   color: $body-color;
   color: $body-color;
-  // text-align: left; // 3
   background-color: $body-bg; // 2
   background-color: $body-bg; // 2
 
 
   svg {
   svg {

+ 9 - 11
packages/app/src/styles/theme/_reboot-bootstrap-theme-colors.scss

@@ -1,8 +1,6 @@
 @use '../bootstrap/init' as *;
 @use '../bootstrap/init' as *;
 @use '../mixins';
 @use '../mixins';
 
 
-$theme-colors: map-merge($theme-colors, $colors);
-
 @each $color, $value in $theme-colors {
 @each $color, $value in $theme-colors {
   @include bg-variant('.bg-#{$color}', $value);
   @include bg-variant('.bg-#{$color}', $value);
 }
 }
@@ -64,15 +62,15 @@ $theme-colors: map-merge($theme-colors, $colors);
   }
   }
 }
 }
 
 
-// @each $color, $value in $theme-colors {
-//   .alert-#{$color} {
-//     @include alert-variant(
-//       theme-color-level($color, $alert-bg-level),
-//       theme-color-level($color, $alert-border-level),
-//       theme-color-level($color, $alert-color-level)
-//     );
-//   }
-// }
+@each $color, $value in $theme-colors {
+  .alert-#{$color} {
+    @include alert-variant(
+      theme-color-level($color, $alert-bg-level),
+      theme-color-level($color, $alert-border-level),
+      theme-color-level($color, $alert-color-level)
+    );
+  }
+}
 
 
 @each $color, $value in $theme-colors {
 @each $color, $value in $theme-colors {
   .badge-#{$color} {
   .badge-#{$color} {

+ 2 - 2
packages/core/src/utils/with-utils.ts

@@ -1,7 +1,7 @@
 import { SWRResponse } from 'swr';
 import { SWRResponse } from 'swr';
 
 
-export type SWRResponseWithUtils<R extends SWRResponse, U> = R & U;
+export type SWRResponseWithUtils<U, D = any, E = any> = SWRResponse<D, E> & U;
 
 
-export const withUtils = <R extends SWRResponse, U>(response: R, utils: U): SWRResponseWithUtils<R, U> => {
+export const withUtils = <U, D = any, E = any>(response: SWRResponse<D, E>, utils: U): SWRResponseWithUtils<U, D, E> => {
   return Object.assign(response, utils);
   return Object.assign(response, utils);
 };
 };

+ 123 - 1
yarn.lock

@@ -4196,6 +4196,11 @@
   resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
   resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
   integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
   integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
 
 
+"@types/parse5@^6.0.0":
+  version "6.0.3"
+  resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-6.0.3.tgz#705bb349e789efa06f43f128cef51240753424cb"
+  integrity sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==
+
 "@types/pixelmatch@^5.2.2":
 "@types/pixelmatch@^5.2.2":
   version "5.2.4"
   version "5.2.4"
   resolved "https://registry.yarnpkg.com/@types/pixelmatch/-/pixelmatch-5.2.4.tgz#ca145cc5ede1388c71c68edf2d1f5190e5ddd0f6"
   resolved "https://registry.yarnpkg.com/@types/pixelmatch/-/pixelmatch-5.2.4.tgz#ca145cc5ede1388c71c68edf2d1f5190e5ddd0f6"
@@ -10444,6 +10449,19 @@ hash-stream-validation@^0.2.2:
   resolved "https://registry.yarnpkg.com/hash-stream-validation/-/hash-stream-validation-0.2.4.tgz#ee68b41bf822f7f44db1142ec28ba9ee7ccb7512"
   resolved "https://registry.yarnpkg.com/hash-stream-validation/-/hash-stream-validation-0.2.4.tgz#ee68b41bf822f7f44db1142ec28ba9ee7ccb7512"
   integrity sha512-Gjzu0Xn7IagXVkSu9cSFuK1fqzwtLwFhNhVL8IFJijRNMgUttFbBSIAzKuSIrsFMO1+g1RlsoN49zPIbwPDMGQ==
   integrity sha512-Gjzu0Xn7IagXVkSu9cSFuK1fqzwtLwFhNhVL8IFJijRNMgUttFbBSIAzKuSIrsFMO1+g1RlsoN49zPIbwPDMGQ==
 
 
+hast-to-hyperscript@^10.0.0:
+  version "10.0.1"
+  resolved "https://registry.yarnpkg.com/hast-to-hyperscript/-/hast-to-hyperscript-10.0.1.tgz#3decd7cb4654bca8883f6fcbd4fb3695628c4296"
+  integrity sha512-dhIVGoKCQVewFi+vz3Vt567E4ejMppS1haBRL6TEmeLeJVB1i/FJIIg/e6s1Bwn0g5qtYojHEKvyGA+OZuyifw==
+  dependencies:
+    "@types/unist" "^2.0.0"
+    comma-separated-tokens "^2.0.0"
+    property-information "^6.0.0"
+    space-separated-tokens "^2.0.0"
+    style-to-object "^0.3.0"
+    unist-util-is "^5.0.0"
+    web-namespaces "^2.0.0"
+
 hast-util-from-parse5@^5.0.0:
 hast-util-from-parse5@^5.0.0:
   version "5.0.3"
   version "5.0.3"
   resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-5.0.3.tgz#3089dc0ee2ccf6ec8bc416919b51a54a589e097c"
   resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-5.0.3.tgz#3089dc0ee2ccf6ec8bc416919b51a54a589e097c"
@@ -10455,6 +10473,20 @@ hast-util-from-parse5@^5.0.0:
     web-namespaces "^1.1.2"
     web-namespaces "^1.1.2"
     xtend "^4.0.1"
     xtend "^4.0.1"
 
 
+hast-util-from-parse5@^7.0.0:
+  version "7.1.0"
+  resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-7.1.0.tgz#c129dd3a24dd8a867ab8a029ca47e27aa54864b7"
+  integrity sha512-m8yhANIAccpU4K6+121KpPP55sSl9/samzQSQGpb0mTExcNh2WlvjtMwSWFhg6uqD4Rr6Nfa8N6TMypQM51rzQ==
+  dependencies:
+    "@types/hast" "^2.0.0"
+    "@types/parse5" "^6.0.0"
+    "@types/unist" "^2.0.0"
+    hastscript "^7.0.0"
+    property-information "^6.0.0"
+    vfile "^5.0.0"
+    vfile-location "^4.0.0"
+    web-namespaces "^2.0.0"
+
 hast-util-has-property@^2.0.0:
 hast-util-has-property@^2.0.0:
   version "2.0.0"
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/hast-util-has-property/-/hast-util-has-property-2.0.0.tgz#c15cd6180f3e535540739fcc9787bcffb5708cae"
   resolved "https://registry.yarnpkg.com/hast-util-has-property/-/hast-util-has-property-2.0.0.tgz#c15cd6180f3e535540739fcc9787bcffb5708cae"
@@ -10472,6 +10504,49 @@ hast-util-parse-selector@^2.0.0:
   resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz#d57c23f4da16ae3c63b3b6ca4616683313499c3a"
   resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz#d57c23f4da16ae3c63b3b6ca4616683313499c3a"
   integrity sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==
   integrity sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==
 
 
+hast-util-parse-selector@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-3.1.0.tgz#a519e27e8b61bd5a98fad494ed06131ce68d9c3f"
+  integrity sha512-AyjlI2pTAZEOeu7GeBPZhROx0RHBnydkQIXlhnFzDi0qfXTmGUWoCYZtomHbrdrheV4VFUlPcfJ6LMF5T6sQzg==
+  dependencies:
+    "@types/hast" "^2.0.0"
+
+hast-util-raw@^7.2.0:
+  version "7.2.1"
+  resolved "https://registry.yarnpkg.com/hast-util-raw/-/hast-util-raw-7.2.1.tgz#6e964cee098dbdd93d1b77cf180b5827d48048ab"
+  integrity sha512-wgtppqXVdXzkDXDFclLLdAyVUJSKMYYi6LWIAbA8oFqEdwksYIcPGM3RkKV1Dfn5GElvxhaOCs0jmCOMayxd3A==
+  dependencies:
+    "@types/hast" "^2.0.0"
+    "@types/parse5" "^6.0.0"
+    hast-util-from-parse5 "^7.0.0"
+    hast-util-to-parse5 "^7.0.0"
+    html-void-elements "^2.0.0"
+    parse5 "^6.0.0"
+    unist-util-position "^4.0.0"
+    unist-util-visit "^4.0.0"
+    vfile "^5.0.0"
+    web-namespaces "^2.0.0"
+    zwitch "^2.0.0"
+
+hast-util-sanitize@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/hast-util-sanitize/-/hast-util-sanitize-4.0.0.tgz#71a02ca2e50d04b852a5500846418070ca364f60"
+  integrity sha512-pw56+69jq+QSr/coADNvWTmBPDy+XsmwaF5KnUys4/wM1jt/fZdl7GPxhXXXYdXnz3Gj3qMkbUCH2uKjvX0MgQ==
+  dependencies:
+    "@types/hast" "^2.0.0"
+
+hast-util-to-parse5@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/hast-util-to-parse5/-/hast-util-to-parse5-7.0.0.tgz#a39808e69005d10afeed1866029a1fb137df3f7c"
+  integrity sha512-YHiS6aTaZ3N0Q3nxaY/Tj98D6kM8QX5Q8xqgg8G45zR7PvWnPGPP0vcKCgb/moIydEJ/QWczVrX0JODCVeoV7A==
+  dependencies:
+    "@types/hast" "^2.0.0"
+    "@types/parse5" "^6.0.0"
+    hast-to-hyperscript "^10.0.0"
+    property-information "^6.0.0"
+    web-namespaces "^2.0.0"
+    zwitch "^2.0.0"
+
 hast-util-to-string@^2.0.0:
 hast-util-to-string@^2.0.0:
   version "2.0.0"
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/hast-util-to-string/-/hast-util-to-string-2.0.0.tgz#b008b0a4ea472bf34dd390b7eea1018726ae152a"
   resolved "https://registry.yarnpkg.com/hast-util-to-string/-/hast-util-to-string-2.0.0.tgz#b008b0a4ea472bf34dd390b7eea1018726ae152a"
@@ -10494,6 +10569,17 @@ hastscript@^5.0.0:
     property-information "^5.0.0"
     property-information "^5.0.0"
     space-separated-tokens "^1.0.0"
     space-separated-tokens "^1.0.0"
 
 
+hastscript@^7.0.0:
+  version "7.0.2"
+  resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-7.0.2.tgz#d811fc040817d91923448a28156463b2e40d590a"
+  integrity sha512-uA8ooUY4ipaBvKcMuPehTAB/YfFLSSzCwFSwT6ltJbocFUKH/GDHLN+tflq7lSRf9H86uOuxOFkh1KgIy3Gg2g==
+  dependencies:
+    "@types/hast" "^2.0.0"
+    comma-separated-tokens "^2.0.0"
+    hast-util-parse-selector "^3.0.0"
+    property-information "^6.0.0"
+    space-separated-tokens "^2.0.0"
+
 header-case@^2.0.3, header-case@^2.0.4:
 header-case@^2.0.3, header-case@^2.0.4:
   version "2.0.4"
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/header-case/-/header-case-2.0.4.tgz#5a42e63b55177349cf405beb8d775acabb92c063"
   resolved "https://registry.yarnpkg.com/header-case/-/header-case-2.0.4.tgz#5a42e63b55177349cf405beb8d775acabb92c063"
@@ -10584,6 +10670,11 @@ html-tags@^3.1.0:
   resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.1.0.tgz#7b5e6f7e665e9fb41f30007ed9e0d41e97fb2140"
   resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.1.0.tgz#7b5e6f7e665e9fb41f30007ed9e0d41e97fb2140"
   integrity sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg==
   integrity sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg==
 
 
+html-void-elements@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-2.0.1.tgz#29459b8b05c200b6c5ee98743c41b979d577549f"
+  integrity sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==
+
 htmlparser2@3.8.x:
 htmlparser2@3.8.x:
   version "3.8.3"
   version "3.8.3"
   resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.8.3.tgz#996c28b191516a8be86501a7d79757e5c70c1068"
   resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.8.3.tgz#996c28b191516a8be86501a7d79757e5c70c1068"
@@ -15869,7 +15960,7 @@ parse5-htmlparser2-tree-adapter@^6.0.0:
   dependencies:
   dependencies:
     parse5 "^6.0.1"
     parse5 "^6.0.1"
 
 
-parse5@6.0.1, parse5@^6.0.1:
+parse5@6.0.1, parse5@^6.0.0, parse5@^6.0.1:
   version "6.0.1"
   version "6.0.1"
   resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
   resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
   integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
   integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
@@ -17544,6 +17635,24 @@ rehype-parse@^6.0.1:
     parse5 "^5.0.0"
     parse5 "^5.0.0"
     xtend "^4.0.0"
     xtend "^4.0.0"
 
 
+rehype-raw@^6.1.1:
+  version "6.1.1"
+  resolved "https://registry.yarnpkg.com/rehype-raw/-/rehype-raw-6.1.1.tgz#81bbef3793bd7abacc6bf8335879d1b6c868c9d4"
+  integrity sha512-d6AKtisSRtDRX4aSPsJGTfnzrX2ZkHQLE5kiUuGOeEoLpbEulFF4hj0mLPbsa+7vmguDKOVVEQdHKDSwoaIDsQ==
+  dependencies:
+    "@types/hast" "^2.0.0"
+    hast-util-raw "^7.2.0"
+    unified "^10.0.0"
+
+rehype-sanitize@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/rehype-sanitize/-/rehype-sanitize-5.0.1.tgz#dac01a7417bdd329260c74c74449697b4be5eb56"
+  integrity sha512-da/jIOjq8eYt/1r9GN6GwxIR3gde7OZ+WV8pheu1tL8K0D9KxM2AyMh+UEfke+FfdM3PvGHeYJU0Td5OWa7L5A==
+  dependencies:
+    "@types/hast" "^2.0.0"
+    hast-util-sanitize "^4.0.0"
+    unified "^10.0.0"
+
 rehype-slug@^5.0.1:
 rehype-slug@^5.0.1:
   version "5.0.1"
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/rehype-slug/-/rehype-slug-5.0.1.tgz#6e732d0c55b3b1e34187e74b7363fb53229e5f52"
   resolved "https://registry.yarnpkg.com/rehype-slug/-/rehype-slug-5.0.1.tgz#6e732d0c55b3b1e34187e74b7363fb53229e5f52"
@@ -21171,6 +21280,14 @@ vfile-location@^2.0.0:
   resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-2.0.6.tgz#8a274f39411b8719ea5728802e10d9e0dff1519e"
   resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-2.0.6.tgz#8a274f39411b8719ea5728802e10d9e0dff1519e"
   integrity sha512-sSFdyCP3G6Ka0CEmN83A2YCMKIieHx0EDaj5IDP4g1pa5ZJ4FJDvpO0WODLxo4LUX4oe52gmSCK7Jw4SBghqxA==
   integrity sha512-sSFdyCP3G6Ka0CEmN83A2YCMKIieHx0EDaj5IDP4g1pa5ZJ4FJDvpO0WODLxo4LUX4oe52gmSCK7Jw4SBghqxA==
 
 
+vfile-location@^4.0.0:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-4.0.1.tgz#06f2b9244a3565bef91f099359486a08b10d3a95"
+  integrity sha512-JDxPlTbZrZCQXogGheBHjbRWjESSPEak770XwWPfw5mTc1v1nWGLB/apzZxsx8a0SJVfF8HK8ql8RD308vXRUw==
+  dependencies:
+    "@types/unist" "^2.0.0"
+    vfile "^5.0.0"
+
 vfile-message@^1.0.0:
 vfile-message@^1.0.0:
   version "1.1.1"
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-1.1.1.tgz#5833ae078a1dfa2d96e9647886cd32993ab313e1"
   resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-1.1.1.tgz#5833ae078a1dfa2d96e9647886cd32993ab313e1"
@@ -21275,6 +21392,11 @@ web-namespaces@^1.1.2:
   resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-1.1.4.tgz#bc98a3de60dadd7faefc403d1076d529f5e030ec"
   resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-1.1.4.tgz#bc98a3de60dadd7faefc403d1076d529f5e030ec"
   integrity sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==
   integrity sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==
 
 
+web-namespaces@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-2.0.1.tgz#1010ff7c650eccb2592cebeeaf9a1b253fd40692"
+  integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==
+
 webidl-conversions@^3.0.0:
 webidl-conversions@^3.0.0:
   version "3.0.1"
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
   resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"