Explorar el Código

Merge branch 'support/apply-nextjs-2' of https://github.com/weseek/growi into feat/100251-render-not-creatable-page

Shun Miyazawa hace 3 años
padre
commit
f7918298d9
Se han modificado 53 ficheros con 424 adiciones y 353 borrados
  1. 1 1
      lerna.json
  2. 1 1
      package.json
  3. 0 1
      packages/app/.env.development
  4. 8 7
      packages/app/package.json
  5. 4 4
      packages/app/src/client/models/MarkdownTable.js
  6. 11 9
      packages/app/src/components/Admin/AdminHome/AdminHome.jsx
  7. 2 1
      packages/app/src/components/Admin/App/AppSettingsPageContents.tsx
  8. 1 1
      packages/app/src/components/Admin/ElasticsearchManagement/StatusTable.jsx
  9. 5 4
      packages/app/src/components/Admin/ImportData/GrowiArchiveSection.jsx
  10. 1 1
      packages/app/src/components/Admin/ImportData/ImportDataPageContents.jsx
  11. 2 4
      packages/app/src/components/Admin/MarkdownSetting/PresentationForm.jsx
  12. 2 4
      packages/app/src/components/Admin/MarkdownSetting/WhiteListInput.jsx
  13. 2 4
      packages/app/src/components/Admin/MarkdownSetting/XssForm.jsx
  14. 4 3
      packages/app/src/components/Admin/UserManagement.jsx
  15. 2 4
      packages/app/src/components/Admin/Users/GiveAdminButton.jsx
  16. 2 4
      packages/app/src/components/Admin/Users/InviteUserControl.jsx
  17. 3 6
      packages/app/src/components/Admin/Users/PasswordResetModal.jsx
  18. 3 5
      packages/app/src/components/Admin/Users/SendInvitationEmailButton.jsx
  19. 2 4
      packages/app/src/components/Admin/Users/StatusActivateButton.jsx
  20. 1 1
      packages/app/src/components/Admin/Users/UserInviteModal.jsx
  21. 2 4
      packages/app/src/components/Admin/Users/UserMenu.jsx
  22. 2 4
      packages/app/src/components/Admin/Users/UserRemoveButton.jsx
  23. 6 3
      packages/app/src/components/Navbar/GrowiNavbar.tsx
  24. 121 136
      packages/app/src/components/Page.jsx
  25. 2 3
      packages/app/src/components/Page/DisplaySwitcher.tsx
  26. 0 1
      packages/app/src/components/Page/RevisionBody.jsx
  27. 13 21
      packages/app/src/components/Page/RevisionRenderer.jsx
  28. 0 1
      packages/app/src/components/PageEditor/Preview.tsx
  29. 7 3
      packages/app/src/components/Sidebar/SidebarNav.tsx
  30. 6 3
      packages/app/src/components/TableOfContents.jsx
  31. 33 0
      packages/app/src/interfaces/activity.ts
  32. 0 2
      packages/app/src/interfaces/global.ts
  33. 9 11
      packages/app/src/interfaces/services/renderer.ts
  34. 35 10
      packages/app/src/pages/[[...path]].page.tsx
  35. 15 19
      packages/app/src/pages/admin/[[...path]].page.tsx
  36. 41 13
      packages/app/src/server/routes/apiv3/slack-integration-settings.js
  37. 0 6
      packages/app/src/server/service/config-loader.ts
  38. 3 2
      packages/app/src/services/renderer/growi-renderer.ts
  39. 0 14
      packages/app/src/services/renderer/markdown-it/blockdiag.js
  40. 18 0
      packages/app/src/services/renderer/markdown-it/blockdiag.ts
  41. 1 7
      packages/app/src/services/renderer/markdown-it/mathjax.js
  42. 6 2
      packages/app/src/services/renderer/markdown-it/plantuml.ts
  43. 13 1
      packages/app/src/stores/context.tsx
  44. 10 3
      packages/app/src/stores/page.tsx
  45. 16 7
      packages/app/src/stores/renderer.tsx
  46. 1 1
      packages/codemirror-textlint/package.json
  47. 1 1
      packages/core/package.json
  48. 1 1
      packages/plugin-attachment-refs/package.json
  49. 1 1
      packages/plugin-lsx/package.json
  50. 1 1
      packages/plugin-pukiwiki-like-linker/package.json
  51. 1 1
      packages/slack/package.json
  52. 1 1
      packages/slackbot-proxy/package.json
  53. 1 1
      packages/ui/package.json

+ 1 - 1
lerna.json

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

+ 1 - 1
package.json

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

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

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

+ 8 - 7
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "5.1.0-RC.1",
+  "version": "5.1.0-RC.2",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -63,11 +63,11 @@
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^5.1.0-RC.1",
-    "@growi/plugin-attachment-refs": "^5.1.0-RC.1",
-    "@growi/plugin-lsx": "^5.1.0-RC.1",
-    "@growi/plugin-pukiwiki-like-linker": "^5.1.0-RC.1",
-    "@growi/slack": "^5.1.0-RC.1",
+    "@growi/codemirror-textlint": "^5.1.0-RC.2",
+    "@growi/plugin-attachment-refs": "^5.1.0-RC.2",
+    "@growi/plugin-lsx": "^5.1.0-RC.2",
+    "@growi/plugin-pukiwiki-like-linker": "^5.1.0-RC.2",
+    "@growi/slack": "^5.1.0-RC.2",
     "@promster/express": "^7.0.2",
     "@promster/server": "^7.0.4",
     "@slack/events-api": "^3.0.0",
@@ -171,7 +171,8 @@
     "handsontable": "v7.0.0 or above is no loger MIT lisence."
   },
   "devDependencies": {
-    "@growi/ui": "^5.1.0-RC.1",
+    "@alienfast/i18next-loader": "^1.1.4",
+    "@growi/ui": "^5.1.0-RC.2",
     "@handsontable/react": "=2.1.0",
     "@types/compression": "^1.7.0",
     "@types/express": "^4.17.11",

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

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

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

@@ -1,19 +1,22 @@
 import React, { useEffect, useCallback } from 'react';
-import PropTypes from 'prop-types';
+
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
 import { Tooltip } from 'reactstrap';
-import loggerFactory from '~/utils/logger';
 
+import AdminHomeContainer from '~/client/services/AdminHomeContainer';
 import { toastError } from '~/client/util/apiNotification';
+import { useSWRxV5MigrationStatus } from '~/stores/page-listing';
+import loggerFactory from '~/utils/logger';
+
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
-import AppContainer from '~/client/services/AppContainer';
-import AdminHomeContainer from '~/client/services/AdminHomeContainer';
-import { useSWRxV5MigrationStatus } from '~/stores/page-listing';
-import SystemInfomationTable from './SystemInfomationTable';
-import InstalledPluginTable from './InstalledPluginTable';
+
+
 import EnvVarsTable from './EnvVarsTable';
+import InstalledPluginTable from './InstalledPluginTable';
+import SystemInfomationTable from './SystemInfomationTable';
 
 const logger = loggerFactory('growi:admin');
 
@@ -129,10 +132,9 @@ const AdminHome = (props) => {
 };
 
 
-const AdminHomeWrapper = withUnstatedContainers(AdminHome, [AppContainer, AdminHomeContainer]);
+const AdminHomeWrapper = withUnstatedContainers(AdminHome, [AdminHomeContainer]);
 
 AdminHome.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminHomeContainer: PropTypes.instanceOf(AdminHomeContainer).isRequired,
 };
 

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

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

+ 1 - 1
packages/app/src/components/Admin/ElasticsearchManagement/StatusTable.jsx

@@ -1,7 +1,7 @@
 import React from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 class StatusTable extends React.PureComponent {
 

+ 5 - 4
packages/app/src/components/Admin/ImportData/GrowiArchiveSection.jsx

@@ -1,14 +1,14 @@
 import React, { Fragment } from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 import * as toastr from 'toastr';
 
 import { apiv3Delete, apiv3Get } from '~/client/util/apiv3-client';
 
 // import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
-import ImportForm from './GrowiArchive/ImportForm';
+// import ImportForm from './GrowiArchive/ImportForm';
 import UploadForm from './GrowiArchive/UploadForm';
 
 class GrowiArchiveSection extends React.Component {
@@ -129,11 +129,12 @@ class GrowiArchiveSection extends React.Component {
         {isTheSameVersion === false && this.renderDefferentVersionAlert()}
         {this.state.fileName != null && isTheSameVersion === true ? (
           <div className="px-4">
-            <ImportForm
+            {/* show ImportForm by https://redmine.weseek.co.jp/issues/100061 */}
+            {/* <ImportForm
               fileName={this.state.fileName}
               innerFileStats={this.state.innerFileStats}
               onDiscard={this.discardData}
-            />
+            /> */}
           </div>
         )
           : (

+ 1 - 1
packages/app/src/components/Admin/ImportData/ImportDataPageContents.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 AdminImportContainer from '~/client/services/AdminImportContainer';
 

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

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

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

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

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

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

+ 4 - 3
packages/app/src/components/Admin/UserManagement.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 AdminUsersContainer from '~/client/services/AdminUsersContainer';
 import { toastError } from '~/client/util/apiNotification';
@@ -10,7 +10,7 @@ import PaginationWrapper from '../PaginationWrapper';
 import { withUnstatedContainers } from '../UnstatedUtils';
 
 
-import InviteUserControl from './Users/InviteUserControl';
+// import InviteUserControl from './Users/InviteUserControl';
 import PasswordResetModal from './Users/PasswordResetModal';
 import UserTable from './Users/UserTable';
 
@@ -150,7 +150,8 @@ class UserManagement extends React.Component {
           />
         )}
         <p>
-          <InviteUserControl />
+          {/* show  */}
+          {/* <InviteUserControl /> */}
           <a className="btn btn-outline-secondary ml-2" href="/admin/users/external-accounts" role="button">
             <i className="icon-user-follow" aria-hidden="true"></i>
             {t('admin:user_management.external_account')}

+ 2 - 4
packages/app/src/components/Admin/Users/GiveAdminButton.jsx

@@ -1,10 +1,9 @@
 import React from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 import AdminUsersContainer from '~/client/services/AdminUsersContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -49,11 +48,10 @@ const GiveAdminButtonWrapperFC = (props) => {
 /**
  * Wrapper component for using unstated
  */
-const GiveAdminButtonWrapper = withUnstatedContainers(GiveAdminButtonWrapperFC, [AppContainer, AdminUsersContainer]);
+const GiveAdminButtonWrapper = withUnstatedContainers(GiveAdminButtonWrapperFC, [AdminUsersContainer]);
 
 GiveAdminButton.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminUsersContainer: PropTypes.instanceOf(AdminUsersContainer).isRequired,
 
   user: PropTypes.object.isRequired,

+ 2 - 4
packages/app/src/components/Admin/Users/InviteUserControl.jsx

@@ -1,10 +1,9 @@
 import React, { Fragment } from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 import AdminUsersContainer from '~/client/services/AdminUsersContainer';
-import AppContainer from '~/client/services/AppContainer';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
@@ -29,7 +28,6 @@ class InviteUserControl extends React.Component {
 
 InviteUserControl.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminUsersContainer: PropTypes.instanceOf(AdminUsersContainer).isRequired,
 };
 
@@ -41,6 +39,6 @@ const InviteUserControlWrapperFC = (props) => {
 /**
  * Wrapper component for using unstated
  */
-const InviteUserControlWrapper = withUnstatedContainers(InviteUserControlWrapperFC, [AppContainer, AdminUsersContainer]);
+const InviteUserControlWrapper = withUnstatedContainers(InviteUserControlWrapperFC, [AdminUsersContainer]);
 
 export default InviteUserControlWrapper;

+ 3 - 6
packages/app/src/components/Admin/Users/PasswordResetModal.jsx

@@ -1,12 +1,11 @@
 import React from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 
-import AppContainer from '~/client/services/AppContainer';
 import { toastError } from '~/client/util/apiNotification';
 import { apiv3Put } from '~/client/util/apiv3-client';
 
@@ -26,7 +25,7 @@ class PasswordResetModal extends React.Component {
   }
 
   async resetPassword() {
-    const { t, appContainer, userForPasswordResetModal } = this.props;
+    const { t, userForPasswordResetModal } = this.props;
     try {
       const res = await apiv3Put('/users/reset-password', { id: userForPasswordResetModal._id });
       const { newPassword } = res.data;
@@ -117,11 +116,9 @@ const PasswordResetModalWrapperFC = (props) => {
 /**
  * Wrapper component for using unstated
  */
-const PasswordResetModalWrapper = withUnstatedContainers(PasswordResetModalWrapperFC, [AppContainer]);
 
 PasswordResetModal.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 
   isOpen: PropTypes.bool.isRequired,
   onClose: PropTypes.func.isRequired,
@@ -129,4 +126,4 @@ PasswordResetModal.propTypes = {
 
 };
 
-export default PasswordResetModalWrapper;
+export default PasswordResetModalWrapperFC;

+ 3 - 5
packages/app/src/components/Admin/Users/SendInvitationEmailButton.jsx

@@ -1,10 +1,9 @@
 import React from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 import AdminUsersContainer from '~/client/services/AdminUsersContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Put } from '~/client/util/apiv3-client';
 
@@ -12,7 +11,7 @@ import { withUnstatedContainers } from '../../UnstatedUtils';
 
 const SendInvitationEmailButton = (props) => {
   const {
-    appContainer, user, isInvitationEmailSended, onSuccessfullySentInvitationEmail,
+    user, isInvitationEmailSended, onSuccessfullySentInvitationEmail,
   } = props;
   const { t } = useTranslation();
 
@@ -46,10 +45,9 @@ const SendInvitationEmailButton = (props) => {
   );
 };
 
-const SendInvitationEmailButtonWrapper = withUnstatedContainers(SendInvitationEmailButton, [AppContainer, AdminUsersContainer]);
+const SendInvitationEmailButtonWrapper = withUnstatedContainers(SendInvitationEmailButton, [AdminUsersContainer]);
 
 SendInvitationEmailButton.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   user: PropTypes.object.isRequired,
   isInvitationEmailSended: PropTypes.bool.isRequired,
   onSuccessfullySentInvitationEmail: PropTypes.func.isRequired,

+ 2 - 4
packages/app/src/components/Admin/Users/StatusActivateButton.jsx

@@ -1,10 +1,9 @@
 import React from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 import AdminUsersContainer from '~/client/services/AdminUsersContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -49,11 +48,10 @@ const StatusActivateFormWrapperFC = (props) => {
 /**
  * Wrapper component for using unstated
  */
-const StatusActivateFormWrapper = withUnstatedContainers(StatusActivateFormWrapperFC, [AppContainer, AdminUsersContainer]);
+const StatusActivateFormWrapper = withUnstatedContainers(StatusActivateFormWrapperFC, [AdminUsersContainer]);
 
 StatusActivateButton.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminUsersContainer: PropTypes.instanceOf(AdminUsersContainer).isRequired,
 
   user: PropTypes.object.isRequired,

+ 1 - 1
packages/app/src/components/Admin/Users/UserInviteModal.jsx

@@ -1,8 +1,8 @@
 import React from 'react';
 
+import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
-import { useTranslation } from 'next-i18next';
 // import Button from 'react-bootstrap/es/Button';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,

+ 2 - 4
packages/app/src/components/Admin/Users/UserMenu.jsx

@@ -1,13 +1,12 @@
 import React, { Fragment } from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 import {
   UncontrolledDropdown, DropdownToggle, DropdownMenu,
 } from 'reactstrap';
 
 import AdminUsersContainer from '~/client/services/AdminUsersContainer';
-import AppContainer from '~/client/services/AppContainer';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
@@ -121,11 +120,10 @@ const UserMenuWrapperFC = (props) => {
   return <UserMenu t={t} {...props} />;
 };
 
-const UserMenuWrapper = withUnstatedContainers(UserMenuWrapperFC, [AppContainer, AdminUsersContainer]);
+const UserMenuWrapper = withUnstatedContainers(UserMenuWrapperFC, [AdminUsersContainer]);
 
 UserMenu.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminUsersContainer: PropTypes.instanceOf(AdminUsersContainer).isRequired,
 
   user: PropTypes.object.isRequired,

+ 2 - 4
packages/app/src/components/Admin/Users/UserRemoveButton.jsx

@@ -1,10 +1,9 @@
 import React from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 import AdminUsersContainer from '~/client/services/AdminUsersContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -44,7 +43,6 @@ class UserRemoveButton extends React.Component {
 
 UserRemoveButton.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminUsersContainer: PropTypes.instanceOf(AdminUsersContainer).isRequired,
 
   user: PropTypes.object.isRequired,
@@ -58,6 +56,6 @@ const UserRemoveButtonWrapperFC = (props) => {
 /**
  * Wrapper component for using unstated
  */
-const UserRemoveButtonWrapper = withUnstatedContainers(UserRemoveButtonWrapperFC, [AppContainer, AdminUsersContainer]);
+const UserRemoveButtonWrapper = withUnstatedContainers(UserRemoveButtonWrapperFC, [AdminUsersContainer]);
 
 export default UserRemoveButtonWrapper;

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

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

+ 121 - 136
packages/app/src/components/Page.jsx

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

+ 2 - 3
packages/app/src/components/Page/DisplaySwitcher.tsx

@@ -17,7 +17,7 @@ import { EditorMode, useEditorMode } from '~/stores/ui';
 import CountBadge from '../Common/CountBadge';
 import PageListIcon from '../Icons/PageListIcon';
 import NotFoundPage from '../NotFoundPage';
-// import Page from '../Page';
+import { Page } from '../Page';
 // import PageEditor from '../PageEditor';
 // import PageEditorByHackmd from '../PageEditorByHackmd';
 import TableOfContents from '../TableOfContents';
@@ -133,8 +133,7 @@ const DisplaySwitcher = (): JSX.Element => {
 
             <div className="flex-grow-1 flex-basis-0 mw-0">
               { isUserPage && <UserInfo pageUser={pageUser} />}
-              {/* { !isNotFound && <Page /> } */}
-              { !isNotFound && revision != null && isPopulated(revision) && revision.body }
+              { !isNotFound && <Page /> }
               { renderNotFoundPage() }
             </div>
 

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

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

+ 13 - 21
packages/app/src/components/Page/RevisionRenderer.jsx

@@ -1,18 +1,17 @@
 import React from 'react';
 
+import { loggerFactory } from '^/../codemirror-textlint/src/utils/logger';
 import PropTypes from 'prop-types';
 
-import AppContainer from '~/client/services/AppContainer';
 import { blinkElem } from '~/client/util/blink-section-header';
 import { addSmoothScrollEvent } from '~/client/util/smooth-scroll';
+import InterceptorManager from '~/services/interceptor-manager';
 import GrowiRenderer from '~/services/renderer/growi-renderer';
+import { useInterceptorManager } from '~/stores/context';
 import { useEditorSettings } from '~/stores/editor';
 
-import { withUnstatedContainers } from '../UnstatedUtils';
-
 import RevisionBody from './RevisionBody';
 
-import { loggerFactory } from '^/../codemirror-textlint/src/utils/logger';
 
 const logger = loggerFactory('components:Page:RevisionRenderer');
 
@@ -45,7 +44,7 @@ class LegacyRevisionRenderer extends React.PureComponent {
 
   componentDidUpdate(prevProps) {
     const { markdown: prevMarkdown, highlightKeywords: prevHighlightKeywords } = prevProps;
-    const { markdown, highlightKeywords } = this.props;
+    const { markdown, highlightKeywords, interceptorManager } = this.props;
 
     // render only when props.markdown is updated
     if (markdown !== prevMarkdown || highlightKeywords !== prevHighlightKeywords) {
@@ -58,8 +57,6 @@ class LegacyRevisionRenderer extends React.PureComponent {
     const HeaderLinkArray = Array.from(HeaderLink);
     addSmoothScrollEvent(HeaderLinkArray, blinkElem);
 
-    const { interceptorManager } = window;
-
     interceptorManager.process('postRenderHtml', this.currentRenderingContext);
   }
 
@@ -130,11 +127,15 @@ class LegacyRevisionRenderer extends React.PureComponent {
 
   async renderHtml() {
     const {
-      appContainer, growiRenderer,
+      interceptorManager,
+      growiRenderer,
       highlightKeywords,
     } = this.props;
 
-    const { interceptorManager } = window;
+    if (interceptorManager == null || growiRenderer == null) {
+      return;
+    }
+
     const context = this.currentRenderingContext;
 
     await interceptorManager.process('preRender', context);
@@ -156,13 +157,9 @@ class LegacyRevisionRenderer extends React.PureComponent {
   }
 
   render() {
-    const config = this.props.appContainer.getConfig();
-    const isMathJaxEnabled = !!config.env.MATHJAX;
-
     return (
       <RevisionBody
         html={this.state.html}
-        isMathJaxEnabled={isMathJaxEnabled}
         additionalClassName={this.props.additionalClassName}
         renderMathJaxOnInit
       />
@@ -172,7 +169,7 @@ class LegacyRevisionRenderer extends React.PureComponent {
 }
 
 LegacyRevisionRenderer.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  interceptorManager: PropTypes.instanceOf(InterceptorManager).isRequired,
   growiRenderer: PropTypes.instanceOf(GrowiRenderer).isRequired,
   markdown: PropTypes.string.isRequired,
   pagePath: PropTypes.string.isRequired,
@@ -181,17 +178,12 @@ LegacyRevisionRenderer.propTypes = {
   editorSettings: PropTypes.any,
 };
 
-/**
- * Wrapper component for using unstated
- */
-const LegacyRevisionRendererWrapper = withUnstatedContainers(LegacyRevisionRenderer, [AppContainer]);
-
-
 // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 const RevisionRenderer = (props) => {
+  const { data: interceptorManager } = useInterceptorManager();
   const { data: editorSettings } = useEditorSettings();
 
-  return <LegacyRevisionRendererWrapper {...props} editorSettings={editorSettings} />;
+  return <LegacyRevisionRenderer {...props} interceptorManager={interceptorManager} editorSettings={editorSettings} />;
 };
 
 RevisionRenderer.propTypes = {

+ 0 - 1
packages/app/src/components/PageEditor/Preview.tsx

@@ -19,7 +19,6 @@ type Props = {
   growiRenderer: GrowiRenderer,
   markdown?: string,
   pagePath?: string,
-  isMathJaxEnabled?: boolean,
   renderMathJaxOnInit?: boolean,
   onScroll?: (scrollTop: number) => void,
 }

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

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

+ 6 - 3
packages/app/src/components/TableOfContents.jsx

@@ -6,6 +6,7 @@ import PropTypes from 'prop-types';
 import PageContainer from '~/client/services/PageContainer';
 import { blinkElem } from '~/client/util/blink-section-header';
 import { addSmoothScrollEvent } from '~/client/util/smooth-scroll';
+import { useGlobalEventEmitter } from '~/stores/context';
 import loggerFactory from '~/utils/logger';
 
 
@@ -25,6 +26,8 @@ const TableOfContents = (props) => {
   const { pageUser } = pageContainer.state;
   const isUserPage = pageUser != null;
 
+  const { data: globalEmitter } = useGlobalEventEmitter();
+
   const [tocHtml, setTocHtml] = useState('');
 
   const calcViewHeight = useCallback(() => {
@@ -56,12 +59,12 @@ const TableOfContents = (props) => {
   // set handler to render ToC
   useEffect(() => {
     const handler = html => setTocHtml(html);
-    window.globalEmitter.on('renderTocHtml', handler);
+    globalEmitter.on('renderTocHtml', handler);
 
     return function cleanup() {
-      window.globalEmitter.removeListener('renderTocHtml', handler);
+      globalEmitter.removeListener('renderTocHtml', handler);
     };
-  }, []);
+  }, [globalEmitter]);
 
   return (
     <StickyStretchableScroller

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

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

+ 0 - 2
packages/app/src/interfaces/global.ts

@@ -4,11 +4,9 @@ import GrowiRenderer from '~/services/renderer/growi-renderer';
 import Xss from '~/services/xss';
 
 import { IGraphViewer } from './graph-viewer';
-import { IInterceptorManager } from './interceptor-manager';
 
 export type CustomWindow = Window
                          & typeof globalThis
-                         & { interceptorManager: IInterceptorManager }
                          & { globalEmitter: EventEmitter }
                          & { GraphViewer: IGraphViewer }
                          & { growiRenderer: GrowiRenderer }

+ 9 - 11
packages/app/src/interfaces/services/renderer.ts

@@ -7,18 +7,16 @@ export type RendererSettings = {
   isIndentSizeForced: boolean,
 };
 
-export type GrowiHydratedEnv = {
-  PLANTUML_URI: string | null,
-  BLOCKDIAG_URI: string | null,
-  DRAWIO_URI: string | null,
-  HACKMD_URI: string | null,
-  MATHJAX: string | null,
-  NO_CDN: string | null,
-  GROWI_CLOUD_URI: string | null,
-  GROWI_APP_ID_FOR_GROWI_CLOUD: string | null,
-}
+// export type GrowiHydratedEnv = {
+//   DRAWIO_URI: string | null,
+//   HACKMD_URI: string | null,
+//   NO_CDN: string | null,
+//   GROWI_CLOUD_URI: string | null,
+//   GROWI_APP_ID_FOR_GROWI_CLOUD: string | null,
+// }
 
 export type GrowiRendererConfig = {
   highlightJsStyleBorder: boolean
-  env: Pick<GrowiHydratedEnv, 'MATHJAX' | 'PLANTUML_URI' | 'BLOCKDIAG_URI'>
+  plantumlUri: string | null,
+  blockdiagUri: string | null,
 } & XssOptionConfig;

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

@@ -1,5 +1,7 @@
 import React, { useEffect } from 'react';
 
+import EventEmitter from 'events';
+
 import { isClient, pagePathUtils, pathUtils } from '@growi/core';
 import ExtensibleCustomError from 'extensible-custom-error';
 import {
@@ -18,12 +20,15 @@ import { CrowiRequest } from '~/interfaces/crowi-request';
 // import { useIndentSize } from '~/stores/editor';
 // import { useRendererSettings } from '~/stores/renderer';
 // import { EditorMode, useEditorMode, useIsMobile } from '~/stores/ui';
+import { CustomWindow } from '~/interfaces/global';
 import { IPageWithMeta } from '~/interfaces/page';
+import { GrowiRendererConfig, RendererSettings } from '~/interfaces/services/renderer';
 import { ISidebarConfig } from '~/interfaces/sidebar-config';
 import { PageModel, PageDocument } from '~/server/models/page';
 import UserUISettings, { UserUISettingsDocument } from '~/server/models/user-ui-settings';
 import Xss from '~/services/xss';
 import { useSWRxCurrentPage, useSWRxPageInfo, useSWRxPage } from '~/stores/page';
+import { useRendererSettings } from '~/stores/renderer';
 import {
   usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed, useCurrentSidebarContents, useCurrentProductNavWidth,
 } from '~/stores/ui';
@@ -47,8 +52,8 @@ import {
   useIsForbidden, useIsNotFound, useIsNotCreatable, useIsTrashPage, useShared, useShareLinkId, useIsSharedUser, useIsAbleToDeleteCompletely,
   useAppTitle, useSiteUrl, useConfidential, useIsEnabledStaleNotification,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsMailerSetup,
-  useAclEnabled, useIsAclEnabled, useHasSlackConfig, useDrawioUri, useHackmdUri, useMathJax,
-  useNoCdn, useEditorConfig, useCsrfToken, useIsSearchScopeChildrenAsDefault, useCurrentPageId, useCurrentPathname, useIsSlackConfigured,
+  useAclEnabled, useIsAclEnabled, useHasSlackConfig, useDrawioUri, useHackmdUri,
+  useNoCdn, useEditorConfig, useCsrfToken, useIsSearchScopeChildrenAsDefault, useCurrentPageId, useCurrentPathname, useIsSlackConfigured, useGrowiRendererConfig,
 } from '../stores/context';
 import { useXss } from '../stores/xss';
 
@@ -98,7 +103,6 @@ type Props = CommonProps & {
   // hasSlackConfig: boolean,
   // drawioUri: string,
   // hackmdUri: string,
-  // mathJax: string,
   // noCdn: string,
   // highlightJsStyle: string,
   // isAllReplyShown: boolean,
@@ -110,6 +114,9 @@ type Props = CommonProps & {
   // adminPreferredIndentSize: number,
   // isIndentSizeForced: boolean,
 
+  rendererSettings: RendererSettings,
+  growiRendererConfig: GrowiRendererConfig,
+
   // UI
   userUISettings: UserUISettingsDocument | null
   // Sidebar
@@ -124,6 +131,11 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
 
   const { data: currentUser } = useCurrentUser(props.currentUser != null ? JSON.parse(props.currentUser) : null);
 
+  // register global EventEmitter
+  if (isClient()) {
+    (window as CustomWindow).globalEmitter = new EventEmitter();
+  }
+
   // commons
   useAppTitle(props.appTitle);
   useSiteUrl(props.siteUrl);
@@ -163,16 +175,14 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
   // useHasSlackConfig(props.hasSlackConfig);
   // useDrawioUri(props.drawioUri);
   // useHackmdUri(props.hackmdUri);
-  // useMathJax(props.mathJax);
   // useNoCdn(props.noCdn);
   // useIndentSize(props.adminPreferredIndentSize);
 
-  // useRendererSettings({
-  //   isEnabledLinebreaks: props.isEnabledLinebreaks,
-  //   isEnabledLinebreaksInComments: props.isEnabledLinebreaksInComments,
-  //   adminPreferredIndentSize: props.adminPreferredIndentSize,
-  //   isIndentSizeForced: props.isIndentSizeForced,
-  // });
+  useRendererSettings(props.rendererSettings);
+  useGrowiRendererConfig(props.growiRendererConfig);
+  // useRendererSettings(props.rendererSettingsStr != null ? JSON.parse(props.rendererSettingsStr) : undefined);
+  // useGrowiRendererConfig(props.growiRendererConfigStr != null ? JSON.parse(props.growiRendererConfigStr) : undefined);
+
 
   // const { data: editorMode } = useEditorMode();
 
@@ -420,6 +430,21 @@ async function injectServerConfigurations(context: GetServerSidePropsContext, pr
   // props.adminPreferredIndentSize = configManager.getConfig('markdown', 'markdown:adminPreferredIndentSize');
   // props.isIndentSizeForced = configManager.getConfig('markdown', 'markdown:isIndentSizeForced');
 
+  props.rendererSettings = {
+    isEnabledLinebreaks: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks'),
+    isEnabledLinebreaksInComments: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments'),
+    adminPreferredIndentSize: configManager.getConfig('markdown', 'markdown:adminPreferredIndentSize'),
+    isIndentSizeForced: configManager.getConfig('markdown', 'markdown:isIndentSizeForced'),
+  };
+  props.growiRendererConfig = {
+    isEnabledXssPrevention: configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention'),
+    attrWhiteList: crowi.xssService.getAttrWhiteList(),
+    tagWhiteList: crowi.xssService.getTagWhiteList(),
+    highlightJsStyleBorder: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
+    plantumlUri: process.env.PLANTUML_URI ?? null,
+    blockdiagUri: process.env.BLOCKDIAG_URI ?? null,
+  };
+
   props.sidebarConfig = {
     isSidebarDrawerMode: configManager.getConfig('crowi', 'customize:isSidebarDrawerMode'),
     isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),

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

@@ -5,6 +5,10 @@ import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import { useRouter } from 'next/router';
 
+import AdminHome from '~/components/Admin/AdminHome/AdminHome';
+import AppSettingsPageContents from '~/components/Admin/App/AppSettingsPageContents';
+import DataImportPageContents from '~/components/Admin/ImportData/ImportDataPageContents';
+import MarkDownSettingContents from '~/components/Admin/MarkdownSetting/MarkDownSettingContents';
 import UserGroupPage from '~/components/Admin/UserGroup/UserGroupPage';
 import UserManagement from '~/components/Admin/UserManagement';
 import AdminLayout from '~/components/AdminLayout';
@@ -13,11 +17,7 @@ import { CommonProps, getServerSideCommonProps, useCustomTitle } from '~/pages/c
 import PluginUtils from '~/server/plugins/plugin-utils';
 import ConfigLoader from '~/server/service/config-loader';
 
-// import AdminHome from '~/components/Admin/AdminHome/AdminHome';
-// import AppSettingsPageContents from '~/components/Admin/App/AppSettingsPageContents';
 // import SecurityManagementContents from '~/components/Admin/Security/SecurityManagementContents';
-// import MarkDownSettingContents from '~/components/Admin/MarkdownSetting/MarkDownSettingContents';
-// import DataImportPageContents from '~/components/Admin/ImportData/ImportDataPageContents';
 // import ExportArchiveDataPage from '~/components/Admin/ExportArchiveDataPage';
 // import ElasticsearchManagement from '~/components/Admin/ElasticsearchManagement/ElasticsearchManagement';
 import {
@@ -56,18 +56,16 @@ const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
   const adminPagesMap = {
     home: {
       title: useCustomTitle(props, t('Wiki Management Home Page')),
-      // component: <AdminHome
-      //   nodeVersion={props.nodeVersion}
-      //   npmVersion={props.npmVersion}
-      //   yarnVersion={props.yarnVersion}
-      //   installedPlugins={props.installedPlugins}
-      // />,
-      component: <>AdminHome</>,
+      component: <AdminHome
+        nodeVersion={props.nodeVersion}
+        npmVersion={props.npmVersion}
+        yarnVersion={props.yarnVersion}
+        installedPlugins={props.installedPlugins}
+      />,
     },
     app: {
       title: useCustomTitle(props, t('App Settings')),
-      // component: <AppSettingsPageContents />,
-      component: <>AppSettingsPageContents</>,
+      component: <AppSettingsPageContents />,
     },
     security: {
       title: useCustomTitle(props, t('security_settings')),
@@ -76,8 +74,7 @@ const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
     },
     markdown: {
       title: useCustomTitle(props, t('Markdown Settings')),
-      // component: <MarkDownSettingContents />,
-      component: <>MarkDownSettingContents</>,
+      component: <MarkDownSettingContents />,
     },
     customize: {
       title: useCustomTitle(props, t('Customize Settings')),
@@ -86,8 +83,8 @@ const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
     },
     importer: {
       title: useCustomTitle(props, t('Import Data')),
-      // component: <DataImportPageContents />,
-      component: <>DataImportPageContents</>,
+      component: <DataImportPageContents />,
+
     },
     export: {
       title: useCustomTitle(props, t('Export Archive Data')),
@@ -104,8 +101,7 @@ const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
     },
     users: {
       title: useCustomTitle(props, t('User_Management')),
-      // component: <UserManagement />,
-      component: <>UserManagement</>,
+      component: <UserManagement />,
     },
     'user-groups': {
       title: useCustomTitle(props, t('UserGroup Management')),

+ 41 - 13
packages/app/src/server/routes/apiv3/slack-integration-settings.js

@@ -1,4 +1,3 @@
-import { SlackbotType, defaultSupportedSlackEventActions } from '@growi/slack';
 
 import { SupportedAction } from '~/interfaces/activity';
 import loggerFactory from '~/utils/logger';
@@ -6,6 +5,8 @@ import loggerFactory from '~/utils/logger';
 import { generateAddActivityMiddleware } from '../../middlewares/add-activity';
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
 
+import { SlackbotType, defaultSupportedSlackEventActions } from '@growi/slack';
+
 
 const {
   getConnectionStatus, getConnectionStatuses,
@@ -310,7 +311,8 @@ module.exports = (crowi) => {
    *           200:
    *             description: Succeeded to put botType setting.
    */
-  router.put('/bot-type', accessTokenParser, loginRequiredStrictly, adminRequired, validator.botType, apiV3FormValidator, async(req, res) => {
+  // eslint-disable-next-line max-len
+  router.put('/bot-type', accessTokenParser, loginRequiredStrictly, adminRequired, addActivity, validator.botType, apiV3FormValidator, async(req, res) => {
     const { currentBotType } = req.body;
 
     if (currentBotType == null) {
@@ -319,6 +321,8 @@ module.exports = (crowi) => {
 
     try {
       await handleBotTypeChanging(req, res, currentBotType);
+
+      activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SLACK_BOT_TYPE_UPDATE });
     }
     catch (error) {
       const msg = 'Error occured in updating Custom bot setting';
@@ -345,9 +349,11 @@ module.exports = (crowi) => {
    *           200:
    *             description: Succeeded to delete botType setting.
    */
-  router.delete('/bot-type', accessTokenParser, loginRequiredStrictly, adminRequired, apiV3FormValidator, async(req, res) => {
+  router.delete('/bot-type', accessTokenParser, loginRequiredStrictly, adminRequired, addActivity, apiV3FormValidator, async(req, res) => {
     try {
       await handleBotTypeChanging(req, res, null);
+
+      activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SLACK_BOT_TYPE_DELETE });
     }
     catch (error) {
       const msg = 'Error occured in resetting all';
@@ -369,7 +375,7 @@ module.exports = (crowi) => {
    *           200:
    *             description: Succeeded to put CustomBotWithoutProxy setting.
    */
-  router.put('/without-proxy/update-settings', loginRequiredStrictly, adminRequired, async(req, res) => {
+  router.put('/without-proxy/update-settings', loginRequiredStrictly, adminRequired, addActivity, async(req, res) => {
     const currentBotType = crowi.configManager.getConfig('crowi', 'slackbot:currentBotType');
     if (currentBotType !== SlackbotType.CUSTOM_WITHOUT_PROXY) {
       const msg = 'Not CustomBotWithoutProxy';
@@ -384,6 +390,9 @@ module.exports = (crowi) => {
     try {
       await updateSlackBotSettings(requestParams);
       crowi.slackIntegrationService.publishUpdatedMessage();
+
+      activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SLACK_WITHOUT_PROXY_SETTINGS_UPDATE });
+
       return res.apiv3();
     }
     catch (error) {
@@ -406,8 +415,8 @@ module.exports = (crowi) => {
    *           200:
    *             description: Succeeded to put CustomBotWithoutProxy permissions.
    */
-
-  router.put('/without-proxy/update-permissions', loginRequiredStrictly, adminRequired, validator.updatePermissionsWithoutProxy, async(req, res) => {
+  // eslint-disable-next-line max-len
+  router.put('/without-proxy/update-permissions', loginRequiredStrictly, adminRequired, addActivity, validator.updatePermissionsWithoutProxy, async(req, res) => {
     const currentBotType = crowi.configManager.getConfig('crowi', 'slackbot:currentBotType');
     if (currentBotType !== SlackbotType.CUSTOM_WITHOUT_PROXY) {
       const msg = 'Not CustomBotWithoutProxy';
@@ -423,6 +432,9 @@ module.exports = (crowi) => {
     try {
       await updateSlackBotSettings(params);
       crowi.slackIntegrationService.publishUpdatedMessage();
+
+      activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SLACK_WITHOUT_PROXY_PERMISSION_UPDATE });
+
       return res.apiv3();
     }
     catch (error) {
@@ -496,7 +508,7 @@ module.exports = (crowi) => {
    *          200:
    *            description: Succeeded to delete access tokens for slack
    */
-  router.delete('/slack-app-integrations/:id', validator.deleteIntegration, apiV3FormValidator, async(req, res) => {
+  router.delete('/slack-app-integrations/:id', validator.deleteIntegration, apiV3FormValidator, addActivity, async(req, res) => {
     const { id } = req.params;
 
     try {
@@ -508,6 +520,8 @@ module.exports = (crowi) => {
         await SlackAppIntegration.updateOne({}, { isPrimary: true });
       }
 
+      activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SLACK_WORKSPACE_DELETE });
+
       return res.apiv3({ response });
     }
     catch (error) {
@@ -517,7 +531,7 @@ module.exports = (crowi) => {
     }
   });
 
-  router.put('/proxy-uri', loginRequiredStrictly, adminRequired, validator.proxyUri, apiV3FormValidator, async(req, res) => {
+  router.put('/proxy-uri', loginRequiredStrictly, adminRequired, addActivity, validator.proxyUri, apiV3FormValidator, async(req, res) => {
     const { proxyUri } = req.body;
 
     const requestParams = { 'slackbot:proxyUri': proxyUri };
@@ -525,6 +539,9 @@ module.exports = (crowi) => {
     try {
       await updateSlackBotSettings(requestParams);
       crowi.slackIntegrationService.publishUpdatedMessage();
+
+      activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SLACK_PROXY_URI_UPDATE });
+
       return res.apiv3({});
     }
     catch (error) {
@@ -549,7 +566,7 @@ module.exports = (crowi) => {
    *            description: Succeeded to make it primary
    */
   // eslint-disable-next-line max-len
-  router.put('/slack-app-integrations/:id/make-primary', loginRequiredStrictly, adminRequired, validator.makePrimary, apiV3FormValidator, async(req, res) => {
+  router.put('/slack-app-integrations/:id/make-primary', loginRequiredStrictly, adminRequired, addActivity, validator.makePrimary, apiV3FormValidator, async(req, res) => {
 
     const { id } = req.params;
 
@@ -571,6 +588,8 @@ module.exports = (crowi) => {
         },
       ]);
 
+      activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SLACK_MAKE_APP_PRIMARY });
+
       return res.apiv3();
     }
     catch (error) {
@@ -594,7 +613,7 @@ module.exports = (crowi) => {
    *            description: Succeeded to regenerate slack app tokens
    */
   // eslint-disable-next-line max-len
-  router.put('/slack-app-integrations/:id/regenerate-tokens', loginRequiredStrictly, adminRequired, validator.regenerateTokens, apiV3FormValidator, async(req, res) => {
+  router.put('/slack-app-integrations/:id/regenerate-tokens', loginRequiredStrictly, adminRequired, addActivity, validator.regenerateTokens, apiV3FormValidator, async(req, res) => {
 
     const { id } = req.params;
 
@@ -602,6 +621,8 @@ module.exports = (crowi) => {
       const { tokenGtoP, tokenPtoG } = await SlackAppIntegration.generateUniqueAccessTokens();
       const slackAppTokens = await SlackAppIntegration.findByIdAndUpdate(id, { tokenGtoP, tokenPtoG });
 
+      activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SLACK_ACCESS_TOKEN_REGENERATE });
+
       return res.apiv3(slackAppTokens, 200);
     }
     catch (error) {
@@ -625,7 +646,7 @@ module.exports = (crowi) => {
    *            description: Succeeded to update supported commands
    */
   // eslint-disable-next-line max-len
-  router.put('/slack-app-integrations/:id/permissions', loginRequiredStrictly, adminRequired, validator.updatePermissionsWithProxy, apiV3FormValidator, async(req, res) => {
+  router.put('/slack-app-integrations/:id/permissions', loginRequiredStrictly, adminRequired, addActivity, validator.updatePermissionsWithProxy, apiV3FormValidator, async(req, res) => {
     // TODO: look here 78975
     const { permissionsForBroadcastUseCommands, permissionsForSingleUseCommands, permissionsForSlackEventActions } = req.body;
     const { id } = req.params;
@@ -659,6 +680,8 @@ module.exports = (crowi) => {
         );
       }
 
+      activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SLACK_PERMISSION_UPDATE });
+
       return res.apiv3({});
     }
     catch (error) {
@@ -682,7 +705,7 @@ module.exports = (crowi) => {
    *             description: Succeeded to delete botType setting.
    */
   // eslint-disable-next-line max-len
-  router.post('/slack-app-integrations/:id/relation-test', loginRequiredStrictly, adminRequired, validator.relationTest, apiV3FormValidator, async(req, res) => {
+  router.post('/slack-app-integrations/:id/relation-test', loginRequiredStrictly, adminRequired, addActivity, validator.relationTest, apiV3FormValidator, async(req, res) => {
     const currentBotType = crowi.configManager.getConfig('crowi', 'slackbot:currentBotType');
     if (currentBotType === SlackbotType.CUSTOM_WITHOUT_PROXY) {
       const msg = 'Not Proxy Type';
@@ -732,6 +755,9 @@ module.exports = (crowi) => {
     catch (error) {
       return res.apiv3Err(new ErrorV3(`Error occured while sending message. Cause: ${error.message}`, 'send-message-failed', error.stack));
     }
+
+    activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SLACK_RELATION_TEST });
+
     return res.apiv3();
 
   });
@@ -756,7 +782,7 @@ module.exports = (crowi) => {
    *           200:
    *             description: Succeeded to connect to slack work space.
    */
-  router.post('/without-proxy/test', loginRequiredStrictly, adminRequired, validator.slackChannel, apiV3FormValidator, async(req, res) => {
+  router.post('/without-proxy/test', loginRequiredStrictly, adminRequired, addActivity, validator.slackChannel, apiV3FormValidator, async(req, res) => {
     const currentBotType = crowi.configManager.getConfig('crowi', 'slackbot:currentBotType');
     if (currentBotType !== SlackbotType.CUSTOM_WITHOUT_PROXY) {
       const msg = 'Select Without Proxy Type';
@@ -778,6 +804,8 @@ module.exports = (crowi) => {
       return res.apiv3Err(new ErrorV3(`Error occured while sending message. Cause: ${error.message}`, 'send-message-failed', error.stack));
     }
 
+    activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SLACK_WITHOUT_PROXY_TEST });
+
     return res.apiv3();
   });
 

+ 0 - 6
packages/app/src/server/service/config-loader.ts

@@ -73,12 +73,6 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
     type:    ValueType.STRING,
     default: null,
   },
-  MATHJAX: {
-    ns:      'crowi',
-    key:     'app:mathJax',
-    type:    ValueType.STRING,
-    default: null,
-  },
   NO_CDN: {
     ns:      'crowi',
     key:     'app:noCdn',

+ 3 - 2
packages/app/src/services/renderer/growi-renderer.ts

@@ -1,3 +1,4 @@
+import { isClient } from '@growi/core';
 import MarkdownIt from 'markdown-it';
 
 import { Nullable } from '~/interfaces/common'; // TODO: Remove this asap when the ContextExtractor is removed
@@ -57,7 +58,7 @@ export default class GrowiRenderer {
     this.growiRendererConfig = growiRendererConfig;
     this.pagePath = pagePath;
 
-    if ((window as CustomWindow).growiRenderer != null) {
+    if (isClient() && (window as CustomWindow).growiRenderer != null) {
       this.preProcessors = (window as CustomWindow).growiRenderer.preProcessors;
       this.postProcessors = (window as CustomWindow).growiRenderer.postProcessors;
     }
@@ -98,7 +99,7 @@ export default class GrowiRenderer {
       new TaskListsConfigurer(),
       new HeaderConfigurer(),
       new EmojiConfigurer(),
-      new MathJaxConfigurer(this.growiRendererConfig),
+      new MathJaxConfigurer(),
       new DrawioViewerConfigurer(),
       new PlantUMLConfigurer(this.growiRendererConfig),
       new BlockdiagConfigurer(this.growiRendererConfig),

+ 0 - 14
packages/app/src/services/renderer/markdown-it/blockdiag.js

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

+ 18 - 0
packages/app/src/services/renderer/markdown-it/blockdiag.ts

@@ -0,0 +1,18 @@
+import { GrowiRendererConfig } from '~/interfaces/services/renderer';
+
+export default class BlockdiagConfigurer {
+
+  generateSourceUrl: string;
+
+  constructor(growiConfig: GrowiRendererConfig) {
+    this.generateSourceUrl = growiConfig.blockdiagUri || 'https://blockdiag-api.com/';
+  }
+
+  configure(md) {
+    md.use(require('markdown-it-blockdiag'), {
+      generateSourceUrl: this.generateSourceUrl,
+      marker: ':::',
+    });
+  }
+
+}

+ 1 - 7
packages/app/src/services/renderer/markdown-it/mathjax.js

@@ -1,13 +1,7 @@
 export default class MathJaxConfigurer {
 
-  constructor(growiConfig) {
-    this.isEnabled = !!growiConfig.env.MATHJAX; // convert to boolean
-  }
-
   configure(md) {
-    if (this.isEnabled) {
-      md.use(require('markdown-it-mathjax')());
-    }
+    md.use(require('markdown-it-mathjax')());
   }
 
 }

+ 6 - 2
packages/app/src/services/renderer/markdown-it/plantuml.js → packages/app/src/services/renderer/markdown-it/plantuml.ts

@@ -1,11 +1,15 @@
 import plantumlEncoder from 'plantuml-encoder';
 import urljoin from 'url-join';
 
+import { GrowiRendererConfig } from '~/interfaces/services/renderer';
+
 export default class PlantUMLConfigurer {
 
-  constructor(growiConfig) {
+  serverUrl: string;
+
+  constructor(growiConfig: GrowiRendererConfig) {
     // Do NOT use HTTPS URL because plantuml.com refuse request except from members
-    this.serverUrl = growiConfig.env.PLANTUML_URI || 'http://plantuml.com/plantuml';
+    this.serverUrl = growiConfig.plantumlUri || 'http://plantuml.com/plantuml';
 
     this.generateSource = this.generateSource.bind(this);
   }

+ 13 - 1
packages/app/src/stores/context.tsx

@@ -1,10 +1,13 @@
-import { pagePathUtils } from '@growi/core';
+import EventEmitter from 'events';
+
 import { Key, SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 
 
 import { SupportedActionType } from '~/interfaces/activity';
+import { CustomWindow } from '~/interfaces/global';
 import { GrowiRendererConfig } from '~/interfaces/services/renderer';
+import InterceptorManager from '~/services/interceptor-manager';
 
 import { TargetAndAncestors } from '../interfaces/page-listing-results';
 import { IUser } from '../interfaces/user';
@@ -15,6 +18,14 @@ import { useStaticSWR } from './use-static-swr';
 type Nullable<T> = T | null;
 
 
+export const useGlobalEventEmitter = (): SWRResponse<EventEmitter, Error> => {
+  return useStaticSWR<EventEmitter, Error>('globalEventEmitter', undefined, { fallbackData: (window as CustomWindow).globalEmitter });
+};
+
+export const useInterceptorManager = (): SWRResponse<InterceptorManager, Error> => {
+  return useStaticSWR<InterceptorManager, Error>('interceptorManager', undefined, { fallbackData: new InterceptorManager() });
+};
+
 export const useCsrfToken = (initialData?: string): SWRResponse<string, Error> => {
   return useStaticSWR<string, Error>('csrfToken', initialData);
 };
@@ -214,6 +225,7 @@ export const useIsGuestUser = (): SWRResponse<boolean, Error> => {
   return useSWRImmutable(
     ['isGuestUser', currentUser],
     (key: Key, currentUser: IUser) => currentUser == null,
+    { fallbackData: currentUser == null },
   );
 };
 

+ 10 - 3
packages/app/src/stores/page.tsx

@@ -14,11 +14,10 @@ import { IPageTagsInfo } from '../interfaces/tag';
 
 import { useCurrentPageId } from './context';
 
-export const useSWRxPage = (pageId?: string, shareLinkId?: string, initialData?: IPageHasId): SWRResponse<IPageHasId, Error> => {
+export const useSWRxPage = (pageId?: string|null, shareLinkId?: string): SWRResponse<IPageHasId, Error> => {
   return useSWR<IPageHasId, Error>(
     pageId != null ? ['/page', pageId, shareLinkId] : null,
     (endpoint, pageId, shareLinkId) => apiv3Get(endpoint, { pageId, shareLinkId }).then(result => result.data.page),
-    { fallbackData: initialData },
   );
 };
 
@@ -32,7 +31,15 @@ export const useSWRxPageByPath = (path?: string): SWRResponse<IPageHasId, Error>
 export const useSWRxCurrentPage = (shareLinkId?: string, initialData?: IPageHasId): SWRResponse<IPageHasId, Error> => {
   const { data: currentPageId } = useCurrentPageId();
 
-  return useSWRxPage(currentPageId ?? undefined, shareLinkId, initialData);
+  const swrResult = useSWRxPage(currentPageId, shareLinkId);
+
+  // use mutate because fallbackData does not work
+  // see: https://github.com/weseek/growi/commit/5038473e8d6028c9c91310e374a7b5f48b921a15
+  if (initialData != null) {
+    swrResult.mutate(initialData);
+  }
+
+  return swrResult;
 };
 
 

+ 16 - 7
packages/app/src/stores/renderer.tsx

@@ -14,17 +14,26 @@ export const useRendererSettings = (initialData?: RendererSettings): SWRResponse
 };
 
 // The base hook with common processes
-const _useRendererBase = (key: string, generator: RendererGenerator): SWRResponse<GrowiRenderer, Error> => {
+const _useRendererBase = (rendererId: string, generator: RendererGenerator): SWRResponse<GrowiRenderer, Error> => {
   const { data: rendererSettings } = useRendererSettings();
   const { data: currentPath } = useCurrentPagePath();
   const { data: growiRendererConfig } = useGrowiRendererConfig();
 
-  return useSWRImmutable(
-    (rendererSettings == null || growiRendererConfig == null || currentPath == null)
-      ? null
-      : [key, rendererSettings, growiRendererConfig, currentPath],
-    (key, rendererSettings, growiRendererConfig, currentPath) => generator(growiRendererConfig, rendererSettings, currentPath),
-  );
+  const isAllDataValid = rendererSettings != null && currentPath != null && growiRendererConfig != null;
+
+  const key = isAllDataValid
+    ? [rendererId, rendererSettings, growiRendererConfig, currentPath]
+    : null;
+
+  const swrResult = useSWRImmutable(key);
+
+  // use mutate because fallbackData does not work
+  // see: https://github.com/weseek/growi/commit/5038473e8d6028c9c91310e374a7b5f48b921a15
+  if (isAllDataValid && swrResult.data == null) {
+    swrResult.mutate(generator(growiRendererConfig, rendererSettings, currentPath));
+  }
+
+  return swrResult;
 };
 
 export const useViewRenderer = (): SWRResponse<GrowiRenderer, Error> => {

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

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

+ 1 - 1
packages/core/package.json

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

+ 1 - 1
packages/plugin-attachment-refs/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/plugin-attachment-refs",
-  "version": "5.1.0-RC.1",
+  "version": "5.1.0-RC.2",
   "description": "GROWI Plugin to add ref/refimg/refs/refsimg tags",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/plugin-lsx/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/plugin-lsx",
-  "version": "5.1.0-RC.1",
+  "version": "5.1.0-RC.2",
   "description": "GROWI plugin to list pages",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/plugin-pukiwiki-like-linker/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/plugin-pukiwiki-like-linker",
-  "version": "5.1.0-RC.1",
+  "version": "5.1.0-RC.2",
   "description": "GROWI plugin to add PukiwikiLikeLinker",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/slack/package.json

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

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

@@ -25,7 +25,7 @@
   },
   "dependencies": {
     "@godaddy/terminus": "^4.9.0",
-    "@growi/slack": "^5.1.0-RC.1",
+    "@growi/slack": "^5.1.0-RC.2",
     "@slack/oauth": "^2.0.1",
     "@slack/web-api": "^6.2.4",
     "@tsed/common": "^6.43.0",

+ 1 - 1
packages/ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/ui",
-  "version": "5.1.0-RC.1",
+  "version": "5.1.0-RC.2",
   "description": "GROWI UI Libraries",
   "license": "MIT",
   "keywords": [