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

merge support/apply-nextjs-2 into current branch

kaori 3 лет назад
Родитель
Сommit
b0debcff16
48 измененных файлов с 492 добавлено и 271 удалено
  1. 1 0
      packages/app/public/static/locales/en_US/translation.json
  2. 1 0
      packages/app/public/static/locales/ja_JP/translation.json
  3. 1 0
      packages/app/public/static/locales/zh_CN/translation.json
  4. 24 24
      packages/app/src/components/Admin/Customize/Customize.jsx
  5. 1 3
      packages/app/src/components/Admin/Customize/CustomizeCssSetting.tsx
  6. 1 3
      packages/app/src/components/Admin/Customize/CustomizeFunctionSetting.tsx
  7. 6 6
      packages/app/src/components/Admin/Customize/CustomizeTitle.jsx
  8. 17 14
      packages/app/src/components/Admin/LegacySlackIntegration/LegacySlackIntegration.jsx
  9. 1 1
      packages/app/src/components/Admin/LegacySlackIntegration/SlackConfiguration.jsx
  10. 7 11
      packages/app/src/components/Admin/SlackIntegration/CustomBotWithProxySettings.jsx
  11. 3 8
      packages/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySecretTokenSection.jsx
  12. 11 9
      packages/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySettings.jsx
  13. 3 9
      packages/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx
  14. 8 11
      packages/app/src/components/Admin/SlackIntegration/OfficialBotSettings.jsx
  15. 2 13
      packages/app/src/components/Admin/SlackIntegration/SlackIntegration.jsx
  16. 8 9
      packages/app/src/components/Admin/SlackIntegration/WithProxyAccordions.jsx
  17. 2 2
      packages/app/src/components/Admin/UserGroup/UserGroupDeleteModal.tsx
  18. 1 1
      packages/app/src/components/BookmarkButtons.tsx
  19. 1 1
      packages/app/src/components/Common/Dropdown/PageItemControl.tsx
  20. 1 1
      packages/app/src/components/LikeButtons.tsx
  21. 5 3
      packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  22. 0 26
      packages/app/src/components/Navbar/GrowiSubNavigation.module.scss
  23. 5 3
      packages/app/src/components/Navbar/GrowiSubNavigation.tsx
  24. 8 4
      packages/app/src/components/Navbar/GrowiSubNavigationSwitcher.jsx
  25. 32 0
      packages/app/src/components/Navbar/GrowiSubNavigationSwitcher.module.scss
  26. 18 5
      packages/app/src/components/Navbar/SubNavButtons.tsx
  27. 12 10
      packages/app/src/components/Page/DisplaySwitcher.tsx
  28. 6 2
      packages/app/src/components/PagePathNav.tsx
  29. 9 12
      packages/app/src/components/Sidebar/CustomSidebar.tsx
  30. 41 0
      packages/app/src/components/Sidebar/RecentChanges.module.scss
  31. 3 2
      packages/app/src/components/Sidebar/RecentChanges.tsx
  32. 2 5
      packages/app/src/components/Sidebar/SidebarContents.tsx
  33. 21 0
      packages/app/src/components/Skelton.tsx
  34. 1 1
      packages/app/src/components/SubscribeButton.tsx
  35. 28 0
      packages/app/src/components/TableOfContents.module.scss
  36. 74 0
      packages/app/src/components/TableOfContents.tsx
  37. 11 10
      packages/app/src/components/Theme/ThemeJadeGreen.module.scss
  38. 8 0
      packages/app/src/components/Theme/ThemeJadeGreen.tsx
  39. 3 0
      packages/app/src/components/Theme/utils/ThemeProvider.tsx
  40. 2 2
      packages/app/src/pages/[[...path]].page.tsx
  41. 13 4
      packages/app/src/pages/admin/[[...path]].page.tsx
  42. 52 7
      packages/app/src/services/renderer/renderer.tsx
  43. 5 0
      packages/app/src/stores/context.tsx
  44. 18 4
      packages/app/src/stores/renderer.tsx
  45. 0 45
      packages/app/src/styles/_recent-changes.scss
  46. 1 0
      packages/app/src/styles/style-next.scss
  47. 7 0
      packages/app/src/styles/theme/_apply-colors-dark.scss
  48. 7 0
      packages/app/src/styles/theme/_apply-colors-light.scss

+ 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": "撤销",

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

@@ -1,10 +1,9 @@
 
-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';
@@ -23,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 />
@@ -66,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>
@@ -75,14 +75,14 @@ function Customize(props) {
       <div className="mb-5">
         <CustomizeScriptSetting />
       </div>
+    */}
     </div>
   );
 }
 
-const CustomizePageWithUnstatedContainer = withUnstatedContainers(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,
 };
 

+ 17 - 14
packages/app/src/components/Admin/LegacySlackIntegration/LegacySlackIntegration.jsx

@@ -1,4 +1,4 @@
-import React, { useMemo, useState } from 'react';
+import React, { useEffect, useMemo, useState } from 'react';
 
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
@@ -10,28 +10,31 @@ import loggerFactory from '~/utils/logger';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
+
 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) {

+ 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,

+ 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;
 

+ 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()}>

+ 41 - 0
packages/app/src/components/Sidebar/RecentChanges.module.scss

@@ -0,0 +1,41 @@
+.grw-recent-changes-resize-button :global {
+  font-size: 12px;
+  line-height: normal;
+  transform: translateY(-2px);
+
+  .custom-control-label::before {
+    padding-left: 16px;
+    content: 'L';
+  }
+
+  .custom-control-input:checked + .custom-control-label::before {
+    padding-left: 5px;
+    content: 'S';
+  }
+}
+
+.list-group-item :global {
+  .grw-recent-changes-item-lower {
+    height: 17.5px;
+  }
+  .footstamp-icon {
+    svg {
+      width: 14px;
+      height: 14px;
+      transform: translateY(-3.5px);
+    }
+  }
+
+  .grw-list-counts {
+    height: 14px;
+    font-size: 12px;
+  }
+
+  .grw-formatted-distance-date {
+    font-size: 10px;
+  }
+
+  .icon-lock {
+    font-size: 14px;
+  }
+}

+ 3 - 2
packages/app/src/components/Sidebar/RecentChanges.tsx

@@ -17,6 +17,7 @@ import FormattedDistanceDate from '../FormattedDistanceDate';
 
 import InfiniteScroll from './InfiniteScroll';
 
+import styles from './RecentChanges.module.scss';
 
 const logger = loggerFactory('growi:History');
 
@@ -72,7 +73,7 @@ const LargePageItem = memo(({ page }: PageItemProps): JSX.Element => {
   });
 
   return (
-    <li className="list-group-item py-3 px-0">
+    <li className={`list-group-item ${styles['list-group-item']} py-3 px-0`}>
       <div className="d-flex w-100">
         <UserPicture user={page.lastUpdateUser} size="md" noTooltip />
         <div className="flex-grow-1 ml-2">
@@ -157,7 +158,7 @@ const RecentChanges = (): JSX.Element => {
           <i className="icon icon-reload"></i>
         </button>
         <div className="d-flex align-items-center">
-          <div className="grw-recent-changes-resize-button custom-control custom-switch ml-1">
+          <div className={`grw-recent-changes-resize-button ${styles['grw-recent-changes-resize-button']} custom-control custom-switch ml-1`}>
             <input
               id="recentChangesResize"
               className="custom-control-input"

+ 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>

+ 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;
+    }
+  }
+}

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

@@ -0,0 +1,74 @@
+import React, { useCallback, useEffect, useState } from 'react';
+
+import ReactMarkdown from 'react-markdown';
+
+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';
+
+// eslint-disable-next-line no-unused-vars
+const logger = loggerFactory('growi:TableOfContents');
+
+const TableOfContents = (): JSX.Element => {
+
+  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 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']);
+
+    // get smaller bottom line of window height - .system-version height - margin 5px) and containerTop
+    let bottom = Math.min(window.innerHeight - 20 - 5, parentBottom);
+
+    if (isUserPage) {
+      // raise the bottom line by the height and margin-top of UserContentLinks
+      bottom -= 45;
+    }
+    // bottom - revisionToc top
+    return bottom - (containerTop + containerPaddingTop);
+  }, [isUserPage]);
+
+  useEffect(() => {
+    const tocDom = document.getElementById('revision-toc-content');
+    if (tocDom == null) { return }
+    const anchorsInToc = Array.from(tocDom.getElementsByTagName('a'));
+    addSmoothScrollEvent(anchorsInToc, blinkElem);
+  }, [tocHtml]);
+
+  return (
+    <StickyStretchableScroller
+      stickyElemSelector=".grw-side-contents-sticky-container"
+      calcViewHeight={calcViewHeight}
+    >
+      <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>
+  );
+
+};
+
+export default TableOfContents;

+ 11 - 10
packages/app/src/components/Theme/ThemeJadeGreen.module.scss

@@ -1,7 +1,8 @@
-@import '../variables';
-@import '../override-bootstrap-variables';
+@use '../../styles/variables' as *;
+@use '../../styles/bootstrap/variables' as *;
+@use '../../styles/theme/mixins/page-editor-mode-manager';
 
-html[light] {
+.theme[data-color-scheme='light'] :global {
   // Theme colors
   $themecolor: #38b48b;
   $themelight: #ffffff;
@@ -70,8 +71,8 @@ html[light] {
   // admin theme box
   $color-theme-color-box: $primary;
 
-  @import 'apply-colors';
-  @import 'apply-colors-light';
+  @import '../../styles/theme/apply-colors';
+  @import '../../styles/theme/apply-colors-light';
 
   // Navs {
   .nav-tabs {
@@ -89,12 +90,12 @@ html[light] {
   // Button
   .btn-group.grw-page-editor-mode-manager {
     .btn.btn-outline-primary {
-      @include btn-page-editor-mode-manager(#ffffff, $primary, $primary, lighten($primary, 20%));
+      @include page-editor-mode-manager.btn-page-editor-mode-manager(#ffffff, $primary, $primary, lighten($primary, 20%));
     }
   }
 }
 
-html[dark] {
+.theme[data-color-scheme='dark'] :global {
   // Theme colors
   $themecolor: #38b48b;
   $themedark: #333333;
@@ -171,8 +172,8 @@ html[dark] {
   // admin theme box
   $color-theme-color-box: $primary;
 
-  @import 'apply-colors';
-  @import 'apply-colors-dark';
+  @import '../../styles/theme/apply-colors';
+  @import '../../styles/theme/apply-colors-dark';
 
   // Navs
   .nav-tabs {
@@ -198,7 +199,7 @@ html[dark] {
   // Button
   .btn-group.grw-page-editor-mode-manager {
     .btn.btn-outline-primary {
-      @include btn-page-editor-mode-manager(#ffffff, $primary, $primary, darken($primary, 20%));
+      @include page-editor-mode-manager.btn-page-editor-mode-manager(#ffffff, $primary, $primary, darken($primary, 20%));
     }
   }
 }

+ 8 - 0
packages/app/src/components/Theme/ThemeJadeGreen.tsx

@@ -0,0 +1,8 @@
+import { ThemeInjector } from './utils/ThemeInjector';
+
+import styles from './ThemeJadeGreen.module.scss';
+
+const ThemeJadeGreen = ({ children }: { children: JSX.Element }): JSX.Element => {
+  return <ThemeInjector className={styles.theme}>{children}</ThemeInjector>;
+};
+export default ThemeJadeGreen;

+ 3 - 0
packages/app/src/components/Theme/utils/ThemeProvider.tsx

@@ -10,6 +10,7 @@ const ThemeAntarctic = dynamic(() => import('../ThemeAntarctic'));
 const ThemeBlackboard = dynamic(() => import('../ThemeBlackboard'));
 const ThemeChristmas = dynamic(() => import('../ThemeChristmas'));
 const ThemeDefault = dynamic(() => import('../ThemeDefault'));
+const ThemeJadeGreen = dynamic(() => import('../ThemeJadeGreen'));
 
 
 type Props = {
@@ -25,6 +26,8 @@ export const ThemeProvider = ({ theme, children }: Props): JSX.Element => {
       return <ThemeBlackboard>{children}</ThemeBlackboard>;
     case GrowiThemes.CHRISTMAS:
       return <ThemeChristmas>{children}</ThemeChristmas>;
+    case GrowiThemes.JADE_GREEN:
+      return <ThemeJadeGreen>{children}</ThemeJadeGreen>;
     default:
       return <ThemeDefault>{children}</ThemeDefault>;
   }

+ 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>

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

@@ -11,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';
@@ -54,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: {
@@ -80,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')),
@@ -100,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')),

+ 52 - 7
packages/app/src/services/renderer/renderer.tsx

@@ -2,7 +2,7 @@ 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';
@@ -233,7 +233,10 @@ const generateCommonOptions: ReactMarkdownOptionsGenerator = (config: RendererCo
   };
 };
 
-export const generateViewOptions: ReactMarkdownOptionsGenerator = (config: RendererConfig): RendererOptions => {
+export const generateViewOptions = (
+    config: RendererConfig,
+    storeTocNode: (node: HtmlElementNode) => void,
+): RendererOptions => {
 
   const options = generateCommonOptions(config);
 
@@ -248,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',
   // }]);
@@ -279,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> => {

+ 0 - 45
packages/app/src/styles/_recent-changes.scss

@@ -1,45 +0,0 @@
-.grw-sidebar-content-header {
-  .grw-recent-changes-resize-button {
-    font-size: 12px;
-    line-height: normal;
-    transform: translateY(-2px);
-
-    .custom-control-label::before {
-      padding-left: 16px;
-      content: 'L';
-    }
-
-    .custom-control-input:checked + .custom-control-label::before {
-      padding-left: 5px;
-      content: 'S';
-    }
-  }
-}
-
-.list-group {
-  .list-group-item {
-    .grw-recent-changes-item-lower {
-      height: 17.5px;
-    }
-    .footstamp-icon {
-      svg {
-        width: 14px;
-        height: 14px;
-        transform: translateY(-3.5px);
-      }
-    }
-
-    .grw-list-counts {
-      height: 14px;
-      font-size: 12px;
-    }
-
-    .grw-formatted-distance-date {
-      font-size: 10px;
-    }
-
-    .icon-lock {
-      font-size: 14px;
-    }
-  }
-}

+ 1 - 0
packages/app/src/styles/style-next.scss

@@ -8,6 +8,7 @@
 
 // import SimpleBar styles
 @import '~simplebar/dist/simplebar.min.css';
+
 // override simplebar-react styles
 @import 'override-simplebar';
 

+ 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;
+}