فهرست منبع

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

yohei0125 3 سال پیش
والد
کامیت
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',
     'comma-separated-tokens',
     'decode-named-character-reference',
+    'html-void-elements',
+    'property-information',
     'space-separated-tokens',
     'trim-lines',
+    'web-namespaces',
+    'vfile',
+    'zwitch',
     'emoticon',
     ...listPrefixedPackages(['remark-', 'rehype-', 'hast-', 'mdast-', 'micromark-', 'micromark-', 'unist-']),
   ];

+ 2 - 0
packages/app/package.json

@@ -154,6 +154,8 @@
     "react-multiline-clamp": "^2.0.0",
     "reconnecting-websocket": "^4.4.0",
     "redis": "^3.0.2",
+    "rehype-raw": "^6.1.1",
+    "rehype-sanitize": "^5.0.1",
     "rehype-slug": "^5.0.1",
     "rehype-toc": "^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",
   "Close": "Close",
   "Shortcuts": "Shortcuts",
+  "CustomSidebar": "Custom Sidebar",
   "eg": "e.g.",
   "add": "Add",
   "Undo": "Undo",

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

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

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

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

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

@@ -1,7 +1,7 @@
 # :tada: Welcome to GROWI
 
 [![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.
 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 へようこそ
 [![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 | ナレッジベースツールです。  
 会社や大学の研究室、サークルでのナレッジ情報を簡単に共有でき、作られたページは誰でも編集が可能です。

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

@@ -1,7 +1,7 @@
 # :tada: 欢迎来到GROWI
 
 [![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 - 一个知识库工具。
 公司、大学实验室和俱乐部的知识可以轻松共享,任何人都可以编辑页面。

+ 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 AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { toastError } from '~/client/util/apiNotification';
 import { toArrayIfNot } from '~/utils/array-utils';
 import loggerFactory from '~/utils/logger';
 
-import { withLoadingSppiner } from '../../SuspenseUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
 import CustomizeCssSetting from './CustomizeCssSetting';
@@ -24,33 +22,33 @@ import CustomizeTitle from './CustomizeTitle';
 
 const logger = loggerFactory('growi:services:AdminCustomizePage');
 
-let retrieveErrors = null;
+const retrieveErrors = null;
 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 (
     <div data-testid="admin-customize">
       <div className="mb-5">
-        <CustomizeLayoutSetting appContainer={appContainer} />
+        <CustomizeLayoutSetting />
       </div>
       <div className="mb-5">
         <CustomizeThemeSetting />
@@ -67,6 +65,7 @@ function Customize(props) {
       <div className="mb-5">
         <CustomizeTitle />
       </div>
+      {/* TODO: show CustomizeHeaderSetting, CustomizeCssSetting and CustomizeScriptSetting by https://redmine.weseek.co.jp/issues/100534
       <div className="mb-5">
         <CustomizeHeaderSetting />
       </div>
@@ -76,14 +75,14 @@ function Customize(props) {
       <div className="mb-5">
         <CustomizeScriptSetting />
       </div>
+    */}
     </div>
   );
 }
 
-const CustomizePageWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(Customize), [AppContainer, AdminCustomizeContainer]);
+const CustomizePageWithUnstatedContainer = withUnstatedContainers(Customize, [AdminCustomizeContainer]);
 
 Customize.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).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 AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -12,7 +11,6 @@ import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import CustomCssEditor from '../CustomCssEditor';
 
 type Props = {
-  appContainer: AppContainer,
   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;

+ 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 AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -14,7 +13,6 @@ import CustomizeFunctionOption from './CustomizeFunctionOption';
 import PagingSizeUncontrolledDropdown from './PagingSizeUncontrolledDropdown';
 
 type Props = {
-  appContainer: AppContainer,
   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;

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

@@ -1,15 +1,16 @@
 /* eslint-disable max-len */
 import React from 'react';
-import PropTypes from 'prop-types';
+
 import { withTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 import { Card, CardBody } from 'reactstrap';
 
-import AppContainer from '~/client/services/AppContainer';
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
-import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
+import { withUnstatedContainers } from '../../UnstatedUtils';
+import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
+
 class CustomizeTitle extends React.Component {
 
   constructor(props) {
@@ -85,11 +86,10 @@ class CustomizeTitle extends React.Component {
 
 }
 
-const CustomizeTitleWrapper = withUnstatedContainers(CustomizeTitle, [AppContainer, AdminCustomizeContainer]);
+const CustomizeTitleWrapper = withUnstatedContainers(CustomizeTitle, [AdminCustomizeContainer]);
 
 CustomizeTitle.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).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 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 { 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';
 
 const logger = loggerFactory('growi:NotificationSetting');
 
-let retrieveErrors = null;
+const retrieveErrors = null;
 function LegacySlackIntegration(props) {
   const { t } = useTranslation();
   const { adminSlackIntegrationLegacyContainer } = props;
 
   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) {
@@ -62,7 +64,7 @@ function LegacySlackIntegration(props) {
   );
 }
 
-const LegacySlackIntegrationWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(LegacySlackIntegration), [AdminSlackIntegrationLegacyContainer]);
+const LegacySlackIntegrationWithUnstatedContainer = withUnstatedContainers(LegacySlackIntegration, [AdminSlackIntegrationLegacyContainer]);
 
 LegacySlackIntegration.propTypes = {
   adminSlackIntegrationLegacyContainer: PropTypes.instanceOf(AdminSlackIntegrationLegacyContainer).isRequired,

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

@@ -1,7 +1,7 @@
 import React from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 import AdminSlackIntegrationLegacyContainer from '~/client/services/AdminSlackIntegrationLegacyContainer';
 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 PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Delete, apiv3Put } from '~/client/util/apiv3-client';
+import { useAppTitle } from '~/stores/context';
 import loggerFactory from '~/utils/logger';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
 
 import CustomBotWithProxyConnectionStatus from './CustomBotWithProxyConnectionStatus';
 import DeleteSlackBotSettingsModal from './DeleteSlackBotSettingsModal';
@@ -19,7 +18,7 @@ const logger = loggerFactory('growi:cli:SlackIntegration:CustomBotWithProxySetti
 
 const CustomBotWithProxySettings = (props) => {
   const {
-    appContainer, slackAppIntegrations, proxyServerUri,
+    slackAppIntegrations, proxyServerUri,
     onClickAddSlackWorkspaceBtn, onPrimaryUpdated,
     connectionStatuses, onUpdateTokens, onSubmitForm,
   } = props;
@@ -27,6 +26,7 @@ const CustomBotWithProxySettings = (props) => {
   const [integrationIdToDelete, setIntegrationIdToDelete] = useState(null);
   const [siteName, setSiteName] = useState('');
   const { t } = useTranslation();
+  const { data: appTitle } = useAppTitle();
 
   // componentDidUpdate
   useEffect(() => {
@@ -86,9 +86,8 @@ const CustomBotWithProxySettings = (props) => {
   };
 
   useEffect(() => {
-    const siteName = appContainer.config.crowi.title;
-    setSiteName(siteName);
-  }, [appContainer]);
+    setSiteName(appTitle);
+  }, [appTitle]);
 
   return (
     <>
@@ -183,14 +182,11 @@ const CustomBotWithProxySettings = (props) => {
   );
 };
 
-const CustomBotWithProxySettingsWrapper = withUnstatedContainers(CustomBotWithProxySettings, [AppContainer]);
-
 CustomBotWithProxySettings.defaultProps = {
   slackAppIntegrations: [],
 };
 
 CustomBotWithProxySettings.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   slackAppIntegrations: PropTypes.array,
   proxyServerUri: PropTypes.string,
   onClickAddSlackWorkspaceBtn: PropTypes.func,
@@ -201,4 +197,4 @@ CustomBotWithProxySettings.propTypes = {
   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 PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Put } from '~/client/util/apiv3-client';
 
@@ -13,7 +12,7 @@ import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 const CustomBotWithoutProxySecretTokenSection = (props) => {
   const {
-    appContainer, slackSigningSecret, slackBotToken, slackSigningSecretEnv, slackBotTokenEnv, onUpdatedSecretToken,
+    slackSigningSecret, slackBotToken, slackSigningSecretEnv, slackBotTokenEnv, onUpdatedSecretToken,
   } = props;
   const { t } = useTranslation();
 
@@ -113,11 +112,7 @@ const CustomBotWithoutProxySecretTokenSection = (props) => {
   );
 };
 
-const CustomBotWithoutProxySecretTokenSectionWrapper = withUnstatedContainers(CustomBotWithoutProxySecretTokenSection, [AppContainer]);
-
 CustomBotWithoutProxySecretTokenSection.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-
   onUpdatedSecretToken: PropTypes.func,
   slackSigningSecret: PropTypes.string,
   slackSigningSecretEnv: PropTypes.string,
@@ -125,4 +120,4 @@ CustomBotWithoutProxySecretTokenSection.propTypes = {
   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 { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
-import AppContainer from '~/client/services/AppContainer';
+
+import { useAppTitle } from '~/stores/context';
+
 import { withUnstatedContainers } from '../../UnstatedUtils';
-import CustomBotWithoutProxySettingsAccordion, { botInstallationStep } from './CustomBotWithoutProxySettingsAccordion';
+
 import CustomBotWithoutProxyConnectionStatus from './CustomBotWithoutProxyConnectionStatus';
+import CustomBotWithoutProxySettingsAccordion, { botInstallationStep } from './CustomBotWithoutProxySettingsAccordion';
 
 const CustomBotWithoutProxySettings = (props) => {
-  const { appContainer, connectionStatuses } = props;
+  const { connectionStatuses } = props;
   const { t } = useTranslation();
+  const { data: appTitle } = useAppTitle();
   const [siteName, setSiteName] = useState('');
 
   useEffect(() => {
-    const siteName = appContainer.config.crowi.title;
-    setSiteName(siteName);
-  }, [appContainer]);
+    setSiteName(appTitle);
+  }, [appTitle]);
 
   const workspaceName = connectionStatuses[props.slackBotToken]?.workspaceName;
 
@@ -58,10 +62,8 @@ const CustomBotWithoutProxySettings = (props) => {
   );
 };
 
-const CustomBotWithoutProxySettingsWrapper = withUnstatedContainers(CustomBotWithoutProxySettings, [AppContainer]);
 
 CustomBotWithoutProxySettings.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 
   slackSigningSecret: PropTypes.string,
   slackSigningSecretEnv: PropTypes.string,
@@ -75,4 +77,4 @@ CustomBotWithoutProxySettings.propTypes = {
   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 PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
-import AppContainer from '~/client/services/AppContainer';
 import { apiv3Post } from '~/client/util/apiv3-client';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
 import Accordion from '../Common/Accordion';
 
 import CustomBotWithoutProxySecretTokenSection from './CustomBotWithoutProxySecretTokenSection';
@@ -25,7 +23,7 @@ export const botInstallationStep = {
 
 const CustomBotWithoutProxySettingsAccordion = (props) => {
   const {
-    appContainer, activeStep, onTestConnectionInvoked,
+    activeStep, onTestConnectionInvoked,
     slackSigningSecret, slackBotToken, slackSigningSecretEnv, slackBotTokenEnv, commandPermission, eventActionsPermission,
   } = props;
   const successMessage = 'Successfully sent to Slack workspace.';
@@ -190,12 +188,8 @@ const CustomBotWithoutProxySettingsAccordion = (props) => {
 };
 
 
-const CustomBotWithoutProxySettingsAccordionWrapper = withUnstatedContainers(CustomBotWithoutProxySettingsAccordion, [AppContainer]);
-
-
 CustomBotWithoutProxySettingsAccordion.propTypes = {
   activeStep: PropTypes.oneOf(Object.values(botInstallationStep)).isRequired,
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 
   onUpdatedSecretToken: PropTypes.func,
   onTestConnectionInvoked: PropTypes.func,
@@ -208,4 +202,4 @@ CustomBotWithoutProxySettingsAccordion.propTypes = {
   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 { SlackbotType } from '@growi/slack';
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Delete, apiv3Put } from '~/client/util/apiv3-client';
+import { useAppTitle } from '~/stores/context';
 import loggerFactory from '~/utils/logger';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
-
 
 import CustomBotWithProxyConnectionStatus from './CustomBotWithProxyConnectionStatus';
 import DeleteSlackBotSettingsModal from './DeleteSlackBotSettingsModal';
@@ -22,13 +20,14 @@ const logger = loggerFactory('growi:cli:SlackIntegration:OfficialBotSettings');
 
 const OfficialBotSettings = (props) => {
   const {
-    appContainer, slackAppIntegrations,
+    slackAppIntegrations,
     onClickAddSlackWorkspaceBtn, onPrimaryUpdated,
     connectionStatuses, onUpdateTokens, onSubmitForm,
   } = props;
   const [siteName, setSiteName] = useState('');
   const [integrationIdToDelete, setIntegrationIdToDelete] = useState(null);
   const { t } = useTranslation();
+  const { data: appTitle } = useAppTitle();
 
   const addSlackAppIntegrationHandler = async() => {
     if (onClickAddSlackWorkspaceBtn != null) {
@@ -69,10 +68,10 @@ const OfficialBotSettings = (props) => {
     }
   };
 
+
   useEffect(() => {
-    const siteName = appContainer.config.crowi.title;
-    setSiteName(siteName);
-  }, [appContainer]);
+    setSiteName(appTitle);
+  }, [appTitle]);
 
   return (
     <>
@@ -151,14 +150,12 @@ const OfficialBotSettings = (props) => {
   );
 };
 
-const OfficialBotSettingsWrapper = withUnstatedContainers(OfficialBotSettings, [AppContainer]);
 
 OfficialBotSettings.defaultProps = {
   slackAppIntegrations: [],
 };
 
 OfficialBotSettings.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 
   slackAppIntegrations: PropTypes.array,
   onClickAddSlackWorkspaceBtn: PropTypes.func,
@@ -169,4 +166,4 @@ OfficialBotSettings.propTypes = {
   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 { SlackbotType } from '@growi/slack';
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
 
 
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import {
   apiv3Delete, apiv3Get, apiv3Post, apiv3Put,
 } from '~/client/util/apiv3-client';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
-
 import BotTypeCard from './BotTypeCard';
 import ConfirmBotChangeModal from './ConfirmBotChangeModal';
 import CustomBotWithProxySettings from './CustomBotWithProxySettings';
@@ -23,9 +19,8 @@ import OfficialBotSettings from './OfficialBotSettings';
 
 const botTypes = Object.values(SlackbotType);
 
-const SlackIntegration = (props) => {
+const SlackIntegration = () => {
 
-  const { appContainer } = props;
   const { t } = useTranslation();
   const [currentBotType, setCurrentBotType] = 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 { SlackbotType } from '@growi/slack';
+import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
-import { useTranslation } from 'next-i18next';
 import { Tooltip } from 'reactstrap';
 
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Put, apiv3Post } from '~/client/util/apiv3-client';
+import { useSiteUrl } from '~/stores/context';
 import loggerFactory from '~/utils/logger';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -145,7 +145,7 @@ const CustomCopyToClipBoard = (props) => {
 
 const GeneratingTokensAndRegisteringProxyServiceProcess = withUnstatedContainers((props) => {
   const { t } = useTranslation();
-  const { appContainer, slackAppIntegrationId } = props;
+  const { slackAppIntegrationId } = props;
 
   const regenerateTokensHandler = async() => {
     try {
@@ -231,7 +231,7 @@ const GeneratingTokensAndRegisteringProxyServiceProcess = withUnstatedContainers
     </div>
 
   );
-}, [AppContainer]);
+}, []);
 
 const TestProcess = ({
   slackAppIntegrationId, onSubmitForm, onSubmitFormFailed, isLatestConnectionSuccess,
@@ -313,6 +313,7 @@ const TestProcess = ({
 
 const WithProxyAccordions = (props) => {
   const { t } = useTranslation();
+  const { data: siteUrl } = useSiteUrl();
   const [isLatestConnectionSuccess, setIsLatestConnectionSuccess] = useState(false);
 
   const submitForm = () => {
@@ -334,7 +335,7 @@ const WithProxyAccordions = (props) => {
     '②': {
       title: 'register_for_growi_official_bot_proxy_service',
       content: <GeneratingTokensAndRegisteringProxyServiceProcess
-        growiUrl={props.appContainer.config.crowi.url}
+        growiUrl={siteUrl}
         slackAppIntegrationId={props.slackAppIntegrationId}
         tokenPtoG={props.tokenPtoG}
         tokenGtoP={props.tokenGtoP}
@@ -373,7 +374,7 @@ const WithProxyAccordions = (props) => {
     '③': {
       title: 'register_for_growi_custom_bot_proxy',
       content: <GeneratingTokensAndRegisteringProxyServiceProcess
-        growiUrl={props.appContainer.config.crowi.url}
+        growiUrl={siteUrl}
         slackAppIntegrationId={props.slackAppIntegrationId}
         tokenPtoG={props.tokenPtoG}
         tokenGtoP={props.tokenGtoP}
@@ -434,9 +435,7 @@ const WithProxyAccordions = (props) => {
 /**
  * Wrapper component for using unstated
  */
-const WithProxyAccordionsWrapper = withUnstatedContainers(WithProxyAccordions, [AppContainer]);
 WithProxyAccordions.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   botType: PropTypes.oneOf(Object.values(SlackbotType)).isRequired,
   slackAppIntegrationId: PropTypes.string.isRequired,
   tokenPtoG: PropTypes.string,
@@ -446,4 +445,4 @@ WithProxyAccordions.propTypes = {
   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}
       >
         <option value="" disabled>{t('admin:user_group_management.delete_modal.dropdown_desc')}</option>
-        {...options}
+        {options}
       </select>
     );
   }, [availableOptions, actionName, handleActionChange, t]);
@@ -164,7 +164,7 @@ const UserGroupDeleteModal: FC<Props> = (props: Props) => {
         onChange={handleGroupChange}
       >
         <option value="" disabled>{defaultOptionText}</option>
-        {...options}
+        {options}
       </select>
     );
   }, [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"
         id="bookmark-button"
         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' : ''}`}
       >
         <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,
   children?: React.ReactNode,
   operationProcessData?: IPageOperationProcessData,

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

@@ -1,10 +1,10 @@
 import React from 'react';
 
 import i18next from 'i18next';
-import PropTypes from 'prop-types';
 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';
 
 class InstallerForm extends React.Component {
@@ -17,31 +17,31 @@ class InstallerForm extends React.Component {
       isSubmittingDisabled: false,
       selectedLang: {},
     };
-    // this.checkUserName = this.checkUserName.bind(this);
+    this.checkUserName = this.checkUserName.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) {
     i18next.changeLanguage(meta.id);
     this.setState({ selectedLang: meta });
@@ -97,7 +97,7 @@ class InstallerForm extends React.Component {
                   value={this.state.selectedLang.id}
                   name="registerForm[app:globalLang]"
                 />
-                <div className="dropdown-menu" aria-labelledby="dropdownLanguage">
+                {/* <div className="dropdown-menu" aria-labelledby="dropdownLanguage">
                   {
                     localeMetadatas.map(meta => (
                       <button
@@ -111,7 +111,7 @@ class InstallerForm extends React.Component {
                       </button>
                     ))
                   }
-                </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 { useGrowiTheme } from '~/stores/context';
+import { Themes, useNextThemes } from '~/stores/use-next-themes';
 
 import { ThemeProvider } from '../Theme/utils/ThemeProvider';
 
@@ -22,7 +22,14 @@ export const RawLayout = ({ children, title, className }: Props): JSX.Element =>
   const { data: growiTheme } = useGrowiTheme();
 
   // 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 (
     <>

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

@@ -50,7 +50,7 @@ const LikeButtons: FC<LikeButtonsProps> = (props: LikeButtonsProps) => {
         type="button"
         id="like-button"
         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' : ''}`}
       >
         <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 { isPopulated } from '@growi/core';
 import { useTranslation } from 'next-i18next';
+import dynamic from 'next/dynamic';
 import { DropdownItem } from 'reactstrap';
 
 import { exportAsMarkdown } from '~/client/services/page-operation';
@@ -26,16 +28,14 @@ import {
   useIsAbleToShowPageEditorModeManager, useIsAbleToShowPageAuthors,
 } from '~/stores/ui';
 
-import { AdditionalMenuItemsRendererProps } from '../Common/Dropdown/PageItemControl';
 import CreateTemplateModal from '../CreateTemplateModal';
 import AttachmentIcon from '../Icons/AttachmentIcon';
 import HistoryIcon from '../Icons/HistoryIcon';
 import PresentationIcon from '../Icons/PresentationIcon';
 import ShareLinkIcon from '../Icons/ShareLinkIcon';
-
+import { Skelton } from '../Skelton';
 
 import { GrowiSubNavigation } from './GrowiSubNavigation';
-import PageEditorModeManager from './PageEditorModeManager';
 import { SubNavButtons } from './SubNavButtons';
 
 
@@ -151,6 +151,8 @@ type GrowiContextualSubNavigationProps = {
 
 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 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 GrowiLogo from '../Icons/GrowiLogo';
 
+import { GlobalSearchProps } from './GlobalSearch';
 import PersonalDropdown from './PersonalDropdown';
 
 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 { t } = useTranslation();
 
@@ -53,7 +46,7 @@ const NavbarRight = memo((): JSX.Element => {
     return (
       <>
         <li className="nav-item">
-          <ShowSkeltonInSSR><InAppNotificationDropdown /></ShowSkeltonInSSR>
+          <InAppNotificationDropdown />
         </li>
 
         <li className="nav-item d-none d-md-block">
@@ -70,11 +63,11 @@ const NavbarRight = memo((): JSX.Element => {
         </li>
 
         <li className="grw-apperance-mode-dropdown nav-item dropdown">
-          <ShowSkeltonInSSR><AppearanceModeDropdown isAuthenticated={isAuthenticated} /></ShowSkeltonInSSR>
+          <AppearanceModeDropdown isAuthenticated={isAuthenticated} />
         </li>
 
         <li className="grw-personal-dropdown nav-item dropdown dropdown-toggle dropdown-toggle-no-caret" data-testid="grw-personal-dropdown">
-          <ShowSkeltonInSSR><PersonalDropdown /></ShowSkeltonInSSR>
+          <PersonalDropdown />
         </li>
       </>
     );
@@ -84,7 +77,7 @@ const NavbarRight = memo((): JSX.Element => {
     return (
       <>
         <li className="grw-apperance-mode-dropdown nav-item dropdown">
-          <ShowSkeltonInSSR><AppearanceModeDropdown isAuthenticated={isAuthenticated} /></ShowSkeltonInSSR>
+          <AppearanceModeDropdown isAuthenticated={isAuthenticated} />
         </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);
   }
 }
-
-/*
- * 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,
 } 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 DrawerToggler from './DrawerToggler';
 
+
 import styles from './GrowiSubNavigation.module.scss';
 
 
@@ -37,7 +38,8 @@ type Props = {
 
 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();
 

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

@@ -1,16 +1,18 @@
 import React, {
   useMemo, useState, useRef, useEffect, useCallback,
 } from 'react';
-import PropTypes from 'prop-types';
 
+import PropTypes from 'prop-types';
 import StickyEvents from 'sticky-events';
 import { debounce } from 'throttle-debounce';
 
-import loggerFactory from '~/utils/logger';
 import { useSidebarCollapsed } from '~/stores/ui';
+import loggerFactory from '~/utils/logger';
 
 import GrowiContextualSubNavigation from './GrowiContextualSubNavigation';
 
+import styles from './GrowiSubNavigationSwitcher.module.scss';
+
 const logger = loggerFactory('growi:cli:GrowiSubNavigationSticky');
 
 
@@ -108,11 +110,13 @@ const GrowiSubNavigationSwitcher = (props) => {
 
   }, [initWidth, initVisible]);
 
+  // ${styles['grw-subnav-switcher']}
+
   return (
-    <div className={`grw-subnav-switcher ${isVisible ? '' : 'grw-subnav-switcher-hidden'}`}>
+    <div className={`${styles['grw-subnav-switcher']} ${isVisible ? '' : 'grw-subnav-switcher-hidden'}`}>
       <div
         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}
         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 dynamic from 'next/dynamic';
+
 import { toggleBookmark, toggleLike, toggleSubscribe } from '~/client/services/page-operation';
 import {
   IPageInfoAll, IPageToDeleteWithMeta, IPageToRenameWithMeta, isIPageInfoForEntity, isIPageInfoForOperation,
@@ -10,13 +12,10 @@ import { IPageForPageDuplicateModal } from '~/stores/modal';
 import { useSWRBookmarkInfo } from '../../stores/bookmark';
 import { useSWRxPageInfo } from '../../stores/page';
 import { useSWRxUsersList } from '../../stores/user';
-import BookmarkButtons from '../BookmarkButtons';
 import {
-  AdditionalMenuItemsRendererProps, ForceHideMenuItems, MenuItemType, PageItemControl,
+  AdditionalMenuItemsRendererProps, ForceHideMenuItems, MenuItemType, PageItemControlProps,
 } from '../Common/Dropdown/PageItemControl';
-import LikeButtons from '../LikeButtons';
-import SubscribeButton from '../SubscribeButton';
-import SeenUserInfo from '../User/SeenUserInfo';
+import { Skelton } from '../Skelton';
 
 
 type CommonProps = {
@@ -52,6 +51,20 @@ const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element
 
   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 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 UserInfo from '../User/UserInfo';
 
+import styles from '../TableOfContents.module.scss';
+
 
 const WIKI_HEADER_LINK = 120;
 
@@ -62,7 +64,14 @@ const DisplaySwitcher = (): JSX.Element => {
     <>
       <TabContent activeTab={editorMode}>
         <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 && (
               <div className="grw-side-contents-container">
@@ -103,8 +112,8 @@ const DisplaySwitcher = (): JSX.Element => {
                   ) }
 
                   <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>
                     <ContentLinkButtons />
                   </div>
@@ -113,13 +122,6 @@ const DisplaySwitcher = (): JSX.Element => {
               </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>
         </TabPane>
         { isEditable && (

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

@@ -1,10 +1,12 @@
 import React, { FC } from 'react';
+
 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 PagePathHierarchicalLink from './PagePathHierarchicalLink';
+
 
 type Props = {
   pagePath: string,
@@ -19,6 +21,8 @@ const PagePathNav: FC<Props> = (props: Props) => {
   } = props;
   const dPagePath = new DevidedPagePath(pagePath, false, true);
 
+  const CopyDropdown = dynamic(() => import('./Page/CopyDropdown'), { ssr: false });
+
   let formerLink;
   let latterLink;
 

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

@@ -1,13 +1,14 @@
 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 { useSWRxPageByPath } from '~/stores/page';
 import { useCustomSidebarOptions } from '~/stores/renderer';
+import loggerFactory from '~/utils/logger';
+
+import RevisionRenderer from '../Page/RevisionRenderer';
+
 
 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: 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">
         <h3 className="mb-0">
-          Custom Sidebar
+          {t('CustomSidebar')}
           <a className="h6 ml-2" href="/Sidebar"><i className="icon-pencil"></i></a>
         </h3>
         <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 { useCurrentSidebarContents } from '~/stores/ui';
 
-// import CustomSidebar from './CustomSidebar';
+import CustomSidebar from './CustomSidebar';
 import PageTree from './PageTree';
 import RecentChanges from './RecentChanges';
 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 => {
   const { data: currentSidebarContents } = useCurrentSidebarContents();
 
@@ -19,8 +17,7 @@ export const SidebarContents = (): JSX.Element => {
       Contents = RecentChanges;
       break;
     case SidebarContentsType.CUSTOM:
-      // Contents = CustomSidebar;
-      Contents = DummyComponent;
+      Contents = CustomSidebar;
       break;
     case SidebarContentsType.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"
         id="subscribe-button"
         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' : ''}`}
       >
         <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
  * @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>
-      <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 PropTypes from 'prop-types';
+import ReactMarkdown from 'react-markdown';
 
-
-import PageContainer from '~/client/services/PageContainer';
 import { blinkElem } from '~/client/util/blink-section-header';
 import { addSmoothScrollEvent } from '~/client/util/smooth-scroll';
+import { useIsUserPage } from '~/stores/context';
+import { useTocOptions } from '~/stores/renderer';
 import loggerFactory from '~/utils/logger';
 
 
 import { StickyStretchableScroller } from './StickyStretchableScroller';
-import { withUnstatedContainers } from './UnstatedUtils';
 
 // eslint-disable-next-line no-unused-vars
 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 { data: rendererOptions } = useTocOptions();
+
   const calcViewHeight = useCallback(() => {
     // calculate absolute top of '#revision-toc' element
     const parentElem = document.querySelector('.grw-side-contents-container');
-    const parentBottom = parentElem.getBoundingClientRect().bottom;
     const containerElem = document.querySelector('#revision-toc');
+    if (parentElem == null || containerElem == null) {
+      return 0;
+    }
+    const parentBottom = parentElem.getBoundingClientRect().bottom;
     const containerTop = containerElem.getBoundingClientRect().top;
     const containerComputedStyle = getComputedStyle(containerElem);
     const containerPaddingTop = parseFloat(containerComputedStyle['padding-top']);
@@ -49,56 +47,28 @@ const TableOfContents = (props) => {
 
   useEffect(() => {
     const tocDom = document.getElementById('revision-toc-content');
+    if (tocDom == null) { return }
     const anchorsInToc = Array.from(tocDom.getElementsByTagName('a'));
     addSmoothScrollEvent(anchorsInToc, blinkElem);
   }, [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 (
     <StickyStretchableScroller
       stickyElemSelector=".grw-side-contents-sticky-container"
       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>
   );
 
 };
 
-/**
- * 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 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);
 
@@ -254,8 +255,7 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
           <GrowiContextualSubNavigation isLinkSharingDisabled={props.disableLinkSharing} />
         </header>
         <div className="d-edit-none">
-          {/* <GrowiSubNavigationSwitcher /> */}
-          GrowiSubNavigationSwitcher
+          <GrowiSubNavigationSwitcher />
         </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 {
   NextPage, GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
@@ -9,9 +11,11 @@ import AdminHome from '~/components/Admin/AdminHome/AdminHome';
 import AppSettingsPageContents from '~/components/Admin/App/AppSettingsPageContents';
 import ExportArchiveDataPage from '~/components/Admin/ExportArchiveDataPage';
 import DataImportPageContents from '~/components/Admin/ImportData/ImportDataPageContents';
+import LegacySlackIntegration from '~/components/Admin/LegacySlackIntegration/LegacySlackIntegration';
 import MarkDownSettingContents from '~/components/Admin/MarkdownSetting/MarkDownSettingContents';
 import NotificationSetting from '~/components/Admin/Notification/NotificationSetting';
 import SecurityManagementContents from '~/components/Admin/Security/SecurityManagementContents';
+import SlackIntegration from '~/components/Admin/SlackIntegration/SlackIntegration';
 import UserGroupPage from '~/components/Admin/UserGroup/UserGroupPage';
 import UserManagement from '~/components/Admin/UserManagement';
 import AdminLayout from '~/components/Layout/AdminLayout';
@@ -52,7 +56,7 @@ const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
   const path = router.query.path || 'home';
   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 = {
     home: {
@@ -78,8 +82,7 @@ const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
     },
     customize: {
       title: useCustomTitle(props, t('Customize Settings')),
-      // component: <CustomizeSettingContents />,
-      component: <>CustomizeSettingContents</>,
+      component: <CustomizeSettingContents />,
     },
     importer: {
       title: useCustomTitle(props, t('Import Data')),
@@ -98,13 +101,21 @@ const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
       title: '',
       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: {
       title: useCustomTitle(props, t('User_Management')),
       component: <UserManagement />,
     },
     'user-groups': {
       title: useCustomTitle(props, t('UserGroup Management')),
-      component: <>user-groups</>,
+      component: <UserGroupPage />,
     },
     search: {
       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
   if (!isInstalled) {
     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);
     return;
   }

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

@@ -1,6 +1,8 @@
 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 toc, { HtmlElementNode } from 'rehype-toc';
+import toc, { HtmlElementNode } from 'rehype-toc';
 import breaks from 'remark-breaks';
 import emoji from 'remark-emoji';
 import footnotes from 'remark-footnotes';
@@ -214,14 +216,27 @@ export interface ReactMarkdownOptionsGenerator {
 const generateCommonOptions: ReactMarkdownOptionsGenerator = (config: RendererConfig): RendererOptions => {
   return {
     remarkPlugins: [gfm],
-    rehypePlugins: [slug],
+    rehypePlugins: [
+      slug,
+      raw,
+      [sanitize, {
+        ...defaultSchema,
+        attributes: {
+          ...defaultSchema.attributes,
+          '*': ['className', 'class'],
+        },
+      }],
+    ],
     components: {
       a: NextLink,
     },
   };
 };
 
-export const generateViewOptions: ReactMarkdownOptionsGenerator = (config: RendererConfig): RendererOptions => {
+export const generateViewOptions = (
+    config: RendererConfig,
+    storeTocNode: (node: HtmlElementNode) => void,
+): RendererOptions => {
 
   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, {
   //   behavior: 'append',
   // }]);
@@ -267,6 +300,30 @@ export const generateViewOptions: ReactMarkdownOptionsGenerator = (config: Rende
   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 => {
   const options = generateCommonOptions(config);
 

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

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

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

@@ -3,13 +3,17 @@ import useSWRImmutable from 'swr/immutable';
 
 import {
   ReactMarkdownOptionsGenerator, RendererOptions,
-  generateViewOptions, generatePreviewOptions, generateCommentPreviewOptions, generateOthersOptions,
+  generatePreviewOptions, generateCommentPreviewOptions, generateOthersOptions,
+  generateViewOptions, generateTocOptions,
 } from '~/services/renderer/renderer';
 
-import { useRendererConfig } from './context';
+
+import { useCurrentPageTocNode, useRendererConfig } from './context';
 
 // 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 isAllDataValid = rendererConfig != null;
@@ -31,7 +35,17 @@ const _useOptionsBase = (rendererId: string, generator: ReactMarkdownOptionsGene
 export const useViewOptions = (): SWRResponse<RendererOptions, Error> => {
   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> => {

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

@@ -223,7 +223,7 @@ type PreferDrawerModeByUserUtils = {
   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 { scheduleToPut } = useUserUISettings();
 

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

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

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

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

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

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

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

@@ -3,10 +3,7 @@
 @import '~bootstrap/scss/utilities';
 @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/variables';
+
+// merge $colors to $theme-colors
+$theme-colors: map-merge($theme-colors, $colors);
+
 @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-base: $font-family-sans-serif;
 
+$font-size-base: 0.875rem;  // 16px -> 14px
 $line-height-base: 1.42857;
 
 $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 '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
 @import '~simple-line-icons';

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

@@ -513,3 +513,10 @@ ul.pagination {
 .grw-modal-head {
   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 {
   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
 //
 
-// // 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
 .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
 //    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;
-  // text-align: left; // 3
   background-color: $body-bg; // 2
 
   svg {

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

@@ -1,8 +1,6 @@
 @use '../bootstrap/init' as *;
 @use '../mixins';
 
-$theme-colors: map-merge($theme-colors, $colors);
-
 @each $color, $value in $theme-colors {
   @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 {
   .badge-#{$color} {

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

@@ -1,7 +1,7 @@
 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);
 };

+ 123 - 1
yarn.lock

@@ -4196,6 +4196,11 @@
   resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
   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":
   version "5.2.4"
   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"
   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:
   version "5.0.3"
   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"
     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:
   version "2.0.0"
   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"
   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:
   version "2.0.0"
   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"
     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:
   version "2.0.4"
   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"
   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:
   version "3.8.3"
   resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.8.3.tgz#996c28b191516a8be86501a7d79757e5c70c1068"
@@ -15869,7 +15960,7 @@ parse5-htmlparser2-tree-adapter@^6.0.0:
   dependencies:
     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"
   resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
   integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
@@ -17544,6 +17635,24 @@ rehype-parse@^6.0.1:
     parse5 "^5.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:
   version "5.0.1"
   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"
   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:
   version "1.1.1"
   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"
   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:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"