Przeglądaj źródła

Merge branch 'support/apply-nextjs-2' into imprv/99346-next-Growi-SubNavigation

yuken 3 lat temu
rodzic
commit
b788a3be1e
60 zmienionych plików z 1547 dodań i 929 usunięć
  1. 1 1
      lerna.json
  2. 1 1
      package.json
  3. 0 1
      packages/app/.env.development
  4. 19 4
      packages/app/next.config.js
  5. 16 19
      packages/app/package.json
  6. 0 2
      packages/app/src/client/legacy/crowi.js
  7. 4 4
      packages/app/src/client/models/MarkdownTable.js
  8. 16 16
      packages/app/src/client/services/ContextExtractor.tsx
  9. 1 0
      packages/app/src/client/util/blink-section-header.ts
  10. 11 9
      packages/app/src/components/Admin/AdminHome/AdminHome.jsx
  11. 2 1
      packages/app/src/components/Admin/App/AppSettingsPageContents.tsx
  12. 1 1
      packages/app/src/components/Admin/ElasticsearchManagement/StatusTable.jsx
  13. 5 4
      packages/app/src/components/Admin/ImportData/GrowiArchiveSection.jsx
  14. 1 1
      packages/app/src/components/Admin/ImportData/ImportDataPageContents.jsx
  15. 2 4
      packages/app/src/components/Admin/MarkdownSetting/PresentationForm.jsx
  16. 2 4
      packages/app/src/components/Admin/MarkdownSetting/WhiteListInput.jsx
  17. 2 4
      packages/app/src/components/Admin/MarkdownSetting/XssForm.jsx
  18. 4 3
      packages/app/src/components/Admin/UserManagement.jsx
  19. 2 4
      packages/app/src/components/Admin/Users/GiveAdminButton.jsx
  20. 2 4
      packages/app/src/components/Admin/Users/InviteUserControl.jsx
  21. 3 6
      packages/app/src/components/Admin/Users/PasswordResetModal.jsx
  22. 3 5
      packages/app/src/components/Admin/Users/SendInvitationEmailButton.jsx
  23. 2 4
      packages/app/src/components/Admin/Users/StatusActivateButton.jsx
  24. 1 1
      packages/app/src/components/Admin/Users/UserInviteModal.jsx
  25. 2 4
      packages/app/src/components/Admin/Users/UserMenu.jsx
  26. 2 4
      packages/app/src/components/Admin/Users/UserRemoveButton.jsx
  27. 6 3
      packages/app/src/components/Navbar/GrowiNavbar.tsx
  28. 141 140
      packages/app/src/components/Page.jsx
  29. 2 3
      packages/app/src/components/Page/DisplaySwitcher.tsx
  30. 0 1
      packages/app/src/components/Page/RevisionBody.jsx
  31. 0 205
      packages/app/src/components/Page/RevisionRenderer.jsx
  32. 177 0
      packages/app/src/components/Page/RevisionRenderer.tsx
  33. 0 1
      packages/app/src/components/PageEditor/Preview.tsx
  34. 7 3
      packages/app/src/components/Sidebar/SidebarNav.tsx
  35. 6 3
      packages/app/src/components/TableOfContents.jsx
  36. 33 0
      packages/app/src/interfaces/activity.ts
  37. 0 2
      packages/app/src/interfaces/global.ts
  38. 9 11
      packages/app/src/interfaces/services/renderer.ts
  39. 39 10
      packages/app/src/pages/[[...path]].page.tsx
  40. 15 19
      packages/app/src/pages/admin/[[...path]].page.tsx
  41. 41 13
      packages/app/src/server/routes/apiv3/slack-integration-settings.js
  42. 0 6
      packages/app/src/server/service/config-loader.ts
  43. 274 242
      packages/app/src/services/renderer/growi-renderer.ts
  44. 0 14
      packages/app/src/services/renderer/markdown-it/blockdiag.js
  45. 18 0
      packages/app/src/services/renderer/markdown-it/blockdiag.ts
  46. 1 7
      packages/app/src/services/renderer/markdown-it/mathjax.js
  47. 6 2
      packages/app/src/services/renderer/markdown-it/plantuml.ts
  48. 17 1
      packages/app/src/stores/context.tsx
  49. 10 3
      packages/app/src/stores/page.tsx
  50. 39 31
      packages/app/src/stores/renderer.tsx
  51. 21 0
      packages/app/src/utils/next.config.utils.js
  52. 1 1
      packages/codemirror-textlint/package.json
  53. 1 1
      packages/core/package.json
  54. 1 1
      packages/plugin-attachment-refs/package.json
  55. 1 1
      packages/plugin-lsx/package.json
  56. 1 1
      packages/plugin-pukiwiki-like-linker/package.json
  57. 1 1
      packages/slack/package.json
  58. 1 1
      packages/slackbot-proxy/package.json
  59. 1 1
      packages/ui/package.json
  60. 572 90
      yarn.lock

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

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

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

+ 16 - 19
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "5.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",
@@ -149,9 +149,16 @@
     "react-dnd-html5-backend": "^14.1.0",
     "react-dom": "^18.2.0",
     "react-image-crop": "^8.3.0",
+    "react-markdown": "^8.0.3",
     "react-multiline-clamp": "^2.0.0",
     "reconnecting-websocket": "^4.4.0",
     "redis": "^3.0.2",
+    "rehype-slug": "^5.0.1",
+    "rehype-toc": "^3.0.2",
+    "remark-breaks": "^3.0.2",
+    "remark-emoji": "^3.0.2",
+    "remark-footnotes": "^4.0.1",
+    "remark-gfm": "^3.0.1",
     "rimraf": "^3.0.0",
     "socket.io": "^4.2.0",
     "stream-to-promise": "^3.0.0",
@@ -171,7 +178,8 @@
     "handsontable": "v7.0.0 or above is no loger MIT lisence."
   },
   "devDependencies": {
-    "@growi/ui": "^5.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",
@@ -199,17 +207,6 @@
     "jquery.cookie": "~1.4.1",
     "jshint": "^2.13.0",
     "load-css-file": "^1.0.0",
-    "markdown-it": "^10.0.0",
-    "markdown-it-blockdiag": "^1.1.1",
-    "markdown-it-drawio-viewer": "^1.3.1",
-    "markdown-it-emoji": "^1.4.0",
-    "markdown-it-emoji-mart": "^0.1.1",
-    "markdown-it-footnote": "^3.0.1",
-    "markdown-it-mathjax": "^2.0.0",
-    "markdown-it-named-headers": "^0.0.4",
-    "markdown-it-plantuml": "^1.3.0",
-    "markdown-it-task-checkbox": "^1.0.6",
-    "markdown-it-toc-and-anchor-with-slugid": "^1.1.4",
     "markdown-table": "^1.1.1",
     "material-icons": "^1.11.3",
     "morgan": "^1.10.0",
@@ -235,7 +232,7 @@
     "socket.io-client": "^4.2.0",
     "sticky-events": "^3.4.11",
     "swagger2openapi": "^5.3.1",
-    "swr": "^1.1.2",
+    "swr": "^1.3.0",
     "throttle-debounce": "^3.0.1",
     "toastr": "^2.1.2",
     "ts-node-dev": "^2.0.0",

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

@@ -1,5 +1,3 @@
-const { blinkElem, blinkSectionHeaderAtBoot } = require('../util/blink-section-header');
-
 /* eslint-disable react/jsx-filename-extension */
 require('jquery.cookie');
 

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

+ 16 - 16
packages/app/src/client/services/ContextExtractor.tsx

@@ -5,7 +5,7 @@ import { pagePathUtils } from '@growi/core';
 
 import { CustomWindow } from '~/interfaces/global';
 import { IUserUISettings } from '~/interfaces/user-ui-settings';
-import { generatePreviewRenderer } from '~/services/renderer/growi-renderer';
+// import { generatePreviewRenderer } from '~/services/renderer/growi-renderer';
 import { useRendererSettings } from '~/stores/renderer';
 import {
   useIsDeviceSmallerThanMd, useIsDeviceSmallerThanLg,
@@ -188,21 +188,21 @@ const ContextExtractorOnce: FC = () => {
 
   // TODO: Remove this code when reveal.js is omitted. see: https://github.com/weseek/growi/pull/6223
   // Do not access this property from other than reveal.js plugins.
-  (window as CustomWindow).previewRenderer = generatePreviewRenderer(
-    {
-      isEnabledXssPrevention: configByContextHydrate.isEnabledXssPrevention,
-      attrWhiteList: configByContextHydrate.attrWhiteList,
-      tagWhiteList: configByContextHydrate.tagWhiteList,
-      highlightJsStyleBorder: configByContextHydrate.highlightJsStyleBorder,
-      env: {
-        MATHJAX: configByContextHydrate.env.MATHJAX,
-        PLANTUML_URI: configByContextHydrate.env.PLANTUML_URI,
-        BLOCKDIAG_URI: configByContextHydrate.env.BLOCKDIAG_URI,
-      },
-    },
-    null,
-    path,
-  );
+  // (window as CustomWindow).previewRenderer = generatePreviewRenderer(
+  //   {
+  //     isEnabledXssPrevention: configByContextHydrate.isEnabledXssPrevention,
+  //     attrWhiteList: configByContextHydrate.attrWhiteList,
+  //     tagWhiteList: configByContextHydrate.tagWhiteList,
+  //     highlightJsStyleBorder: configByContextHydrate.highlightJsStyleBorder,
+  //     env: {
+  //       MATHJAX: configByContextHydrate.env.MATHJAX,
+  //       PLANTUML_URI: configByContextHydrate.env.PLANTUML_URI,
+  //       BLOCKDIAG_URI: configByContextHydrate.env.BLOCKDIAG_URI,
+  //     },
+  //   },
+  //   null,
+  //   path,
+  // );
 
   return null;
 };

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

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

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

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

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

+ 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';
@@ -114,8 +114,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 /> }
               { isNotFound && <NotFoundPage /> }
             </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,

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

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

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

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

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

+ 39 - 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';
@@ -46,10 +51,11 @@ import {
   useCurrentUser, useCurrentPagePath,
   useOwnerOfCurrentPage, useIsLatestRevision,
   useIsForbidden, useIsNotFound, useIsTrashPage, useShared, useShareLinkId, useIsSharedUser, useIsAbleToDeleteCompletely,
-  useAppTitle, useSiteUrl, useConfidential, useIsEnabledStaleNotification, useIsNotFoundPermalink, useIsNotCreatable,
-  useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsMailerSetup, useIsIdenticalPath, useIsUserPage,
-  useAclEnabled, useIsAclEnabled, useHasSlackConfig, useDrawioUri, useHackmdUri, useMathJax, useDisableLinkSharing,
-  useNoCdn, useEditorConfig, useCsrfToken, useIsSearchScopeChildrenAsDefault, useCurrentPageId, useCurrentPathname, useIsSlackConfigured,
+  useAppTitle, useSiteUrl, useConfidential, useIsEnabledStaleNotification, useIsNotFoundPermalink, useIsIdenticalPath
+  useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsMailerSetup, useDisableLinkSharing,
+  useAclEnabled, useIsAclEnabled, useHasSlackConfig, useDrawioUri, useHackmdUri, useIsUserPage, useIsNotCreatable,
+  useNoCdn, useEditorConfig, useCsrfToken, useIsSearchScopeChildrenAsDefault, useCurrentPageId, useCurrentPathname,
+  useIsSlackConfigured, useGrowiRendererConfig, useIsBlinkedHeaderAtBoot,
 } from '../stores/context';
 import { useXss } from '../stores/xss';
 
@@ -111,6 +117,9 @@ type Props = CommonProps & {
   // isIndentSizeForced: boolean,
   disableLinkSharing: boolean,
 
+  rendererSettings: RendererSettings,
+  growiRendererConfig: GrowiRendererConfig,
+
   // UI
   userUISettings: UserUISettingsDocument | null
   // Sidebar
@@ -125,6 +134,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);
@@ -155,6 +169,7 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
   // useIsAbleToDeleteCompletely(props.isAbleToDeleteCompletely);
   useIsSharedUser(false); // this page cann't be routed for '/share'
   useIsEnabledStaleNotification(props.isEnabledStaleNotification);
+  useIsBlinkedHeaderAtBoot(false);
 
   useIsSearchServiceConfigured(props.isSearchServiceConfigured);
   useIsSearchServiceReachable(props.isSearchServiceReachable);
@@ -171,12 +186,11 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
   // useIndentSize(props.adminPreferredIndentSize);
   useDisableLinkSharing(props.disableLinkSharing);
 
-  // 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();
 
@@ -421,6 +435,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',

+ 274 - 242
packages/app/src/services/renderer/growi-renderer.ts

@@ -1,273 +1,305 @@
-import MarkdownIt from 'markdown-it';
+import { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
+import slug from 'rehype-slug';
+// import toc, { HtmlElementNode } from 'rehype-toc';
+import breaks from 'remark-breaks';
+import emoji from 'remark-emoji';
+import footnotes from 'remark-footnotes';
+import gfm from 'remark-gfm';
 
-import { Nullable } from '~/interfaces/common'; // TODO: Remove this asap when the ContextExtractor is removed
-import { CustomWindow } from '~/interfaces/global';
 import { GrowiRendererConfig, RendererSettings } from '~/interfaces/services/renderer';
 import loggerFactory from '~/utils/logger';
 
-import CsvToTable from './PreProcessor/CsvToTable';
-import EasyGrid from './PreProcessor/EasyGrid';
-import Linker from './PreProcessor/Linker';
-import XssFilter from './PreProcessor/XssFilter';
-import BlockdiagConfigurer from './markdown-it/blockdiag';
-import DrawioViewerConfigurer from './markdown-it/drawio-viewer';
-import EmojiConfigurer from './markdown-it/emoji';
-import FooternoteConfigurer from './markdown-it/footernote';
-import HeaderConfigurer from './markdown-it/header';
-import HeaderLineNumberConfigurer from './markdown-it/header-line-number';
-import HeaderWithEditLinkConfigurer from './markdown-it/header-with-edit-link';
-import LinkerByRelativePathConfigurer from './markdown-it/link-by-relative-path';
-import MathJaxConfigurer from './markdown-it/mathjax';
-import PlantUMLConfigurer from './markdown-it/plantuml';
-import TableConfigurer from './markdown-it/table';
-import TableWithHandsontableButtonConfigurer from './markdown-it/table-with-handsontable-button';
-import TaskListsConfigurer from './markdown-it/task-lists';
-import TocAndAnchorConfigurer from './markdown-it/toc-and-anchor';
+// import CsvToTable from './PreProcessor/CsvToTable';
+// import EasyGrid from './PreProcessor/EasyGrid';
+// import Linker from './PreProcessor/Linker';
+// import XssFilter from './PreProcessor/XssFilter';
+// import BlockdiagConfigurer from './markdown-it/blockdiag';
+// import DrawioViewerConfigurer from './markdown-it/drawio-viewer';
+// import EmojiConfigurer from './markdown-it/emoji';
+// import FooternoteConfigurer from './markdown-it/footernote';
+// import HeaderConfigurer from './markdown-it/header';
+// import HeaderLineNumberConfigurer from './markdown-it/header-line-number';
+// import HeaderWithEditLinkConfigurer from './markdown-it/header-with-edit-link';
+// import LinkerByRelativePathConfigurer from './markdown-it/link-by-relative-path';
+// import MathJaxConfigurer from './markdown-it/mathjax';
+// import PlantUMLConfigurer from './markdown-it/plantuml';
+// import TableConfigurer from './markdown-it/table';
+// import TableWithHandsontableButtonConfigurer from './markdown-it/table-with-handsontable-button';
+// import TaskListsConfigurer from './markdown-it/task-lists';
+// import TocAndAnchorConfigurer from './markdown-it/toc-and-anchor';
 
 
 const logger = loggerFactory('growi:util:GrowiRenderer');
 
-declare const hljs;
+// declare const hljs;
+
+// type MarkdownSettings = {
+//   breaks?: boolean,
+// };
+
+// export default class GrowiRenderer {
+
+//   growiRendererConfig: GrowiRendererConfig;
+
+//   constructor(growiRendererConfig: GrowiRendererConfig, pagePath?: Nullable<string>) {
+//     this.growiRendererConfig = growiRendererConfig;
+//     this.pagePath = pagePath;
+
+//     if (isClient() && (window as CustomWindow).growiRenderer != null) {
+//       this.preProcessors = (window as CustomWindow).growiRenderer.preProcessors;
+//       this.postProcessors = (window as CustomWindow).growiRenderer.postProcessors;
+//     }
+//     else {
+//       this.preProcessors = [
+//         new EasyGrid(),
+//         new Linker(),
+//         new CsvToTable(),
+//         new XssFilter({
+//           isEnabledXssPrevention: this.growiRendererConfig.isEnabledXssPrevention,
+//           tagWhiteList: this.growiRendererConfig.tagWhiteList,
+//           attrWhiteList: this.growiRendererConfig.attrWhiteList,
+//         }),
+//       ];
+//       this.postProcessors = [
+//       ];
+//     }
+
+//     this.init = this.init.bind(this);
+//     this.addConfigurers = this.addConfigurers.bind(this);
+//     this.setMarkdownSettings = this.setMarkdownSettings.bind(this);
+//     this.configure = this.configure.bind(this);
+//     this.process = this.process.bind(this);
+//     this.codeRenderer = this.codeRenderer.bind(this);
+//   }
+
+//   init() {
+//     let parser: Processor = unified().use(parse);
+//     this.remarkPlugins.forEach((item) => {
+//       parser = applyPlugin(parser, item);
+//     });
+
+//     let rehype: Processor = parser.use(remark2rehype);
+//     this.rehypePlugins.forEach((item) => {
+//       rehype = applyPlugin(rehype, item);
+//     });
+
+//     this.processor = rehype.use(rehype2react, {
+//       createElement: React.createElement,
+//       components: {
+//         // a: NextLink,
+//       },
+//     });
+//   }
+
+//   init() {
+//     // init markdown-it
+//     this.md = new MarkdownIt({
+//       html: true,
+//       linkify: true,
+//       highlight: this.codeRenderer,
+//     });
+
+//     this.isMarkdownItConfigured = false;
+
+//     this.markdownItConfigurers = [
+//       new TaskListsConfigurer(),
+//       new HeaderConfigurer(),
+//       new EmojiConfigurer(),
+//       new MathJaxConfigurer(),
+//       new DrawioViewerConfigurer(),
+//       new PlantUMLConfigurer(this.growiRendererConfig),
+//       new BlockdiagConfigurer(this.growiRendererConfig),
+//     ];
+
+//     if (this.pagePath != null) {
+//       this.markdownItConfigurers.push(
+//         new LinkerByRelativePathConfigurer(this.pagePath),
+//       );
+//     }
+//   }
+
+//   addConfigurers(configurers: any[]): void {
+//     this.markdownItConfigurers.push(...configurers);
+//   }
+
+//   setMarkdownSettings(settings: MarkdownSettings): void {
+//     this.md.set(settings);
+//   }
+
+//   configure(): void {
+//     if (!this.isMarkdownItConfigured) {
+//       this.markdownItConfigurers.forEach((configurer) => {
+//         configurer.configure(this.md);
+//       });
+//     }
+//   }
+
+//   preProcess(markdown, context) {
+//     let processed = markdown;
+//     for (let i = 0; i < this.preProcessors.length; i++) {
+//       if (!this.preProcessors[i].process) {
+//         continue;
+//       }
+//       processed = this.preProcessors[i].process(processed, context);
+//     }
+
+//     return processed;
+//   }
+
+//   process(markdown, context) {
+//     return this.md.render(markdown, context);
+//   }
+
+//   postProcess(html, context) {
+//     let processed = html;
+//     for (let i = 0; i < this.postProcessors.length; i++) {
+//       if (!this.postProcessors[i].process) {
+//         continue;
+//       }
+//       processed = this.postProcessors[i].process(processed, context);
+//     }
+
+//     return processed;
+//   }
+
+//   codeRenderer(code, langExt) {
+//     const noborder = (!this.growiRendererConfig.highlightJsStyleBorder) ? 'hljs-no-border' : '';
+
+//     let citeTag = '';
+//     let hljsLang = 'plaintext';
+//     let showLinenumbers = false;
+
+//     if (langExt) {
+//       // https://regex101.com/r/qGs7eZ/3
+//       const match = langExt.match(/^([^:=\n]+)?(=([^:=\n]*))?(:([^:=\n]*))?(=([^:=\n]*))?$/);
+
+//       const lang = match[1];
+//       const fileName = match[5] || null;
+//       showLinenumbers = (match[2] != null) || (match[6] != null);
+
+//       if (fileName != null) {
+//         citeTag = `<cite>${fileName}</cite>`;
+//       }
+//       if (hljs.getLanguage(lang)) {
+//         hljsLang = lang;
+//       }
+//     }
+
+//     let highlightCode = code;
+//     try {
+//       highlightCode = hljs.highlight(hljsLang, code, true).value;
+
+//       // add line numbers
+//       if (showLinenumbers) {
+//         highlightCode = hljs.lineNumbersValue((highlightCode));
+//       }
+//     }
+//     catch (err) {
+//       logger.error(err);
+//     }
+
+//     return `<pre class="hljs ${noborder}">${citeTag}<code>${highlightCode}</code></pre>`;
+//   }
+
+// }
+
+export type RendererOptions = Partial<ReactMarkdownOptions>;
+
+export interface ReactMarkdownOptionsGenerator {
+  (growiRendererConfig: GrowiRendererConfig, rendererSettings: RendererSettings): RendererOptions
+}
 
-type MarkdownSettings = {
-  breaks?: boolean,
+const generateCommonOptions: ReactMarkdownOptionsGenerator = (
+    growiRendererConfig: GrowiRendererConfig, rendererSettings: RendererSettings,
+): RendererOptions => {
+  return {
+    remarkPlugins: [gfm],
+    rehypePlugins: [slug],
+  };
 };
 
-export default class GrowiRenderer {
-
-  preProcessors: any[];
-
-  postProcessors: any[];
-
-  md: any;
-
-  isMarkdownItConfigured: boolean;
-
-  markdownItConfigurers: any[];
-
-  growiRendererConfig: GrowiRendererConfig;
-
-  pagePath?: Nullable<string>;
-
-  /**
-   *
-   * @param {string} mode
-   */
-  constructor(growiRendererConfig: GrowiRendererConfig, pagePath?: Nullable<string>) {
-    this.growiRendererConfig = growiRendererConfig;
-    this.pagePath = pagePath;
-
-    if ((window as CustomWindow).growiRenderer != null) {
-      this.preProcessors = (window as CustomWindow).growiRenderer.preProcessors;
-      this.postProcessors = (window as CustomWindow).growiRenderer.postProcessors;
-    }
-    else {
-      this.preProcessors = [
-        new EasyGrid(),
-        new Linker(),
-        new CsvToTable(),
-        new XssFilter({
-          isEnabledXssPrevention: this.growiRendererConfig.isEnabledXssPrevention,
-          tagWhiteList: this.growiRendererConfig.tagWhiteList,
-          attrWhiteList: this.growiRendererConfig.attrWhiteList,
-        }),
-      ];
-      this.postProcessors = [
-      ];
-    }
-
-    this.init = this.init.bind(this);
-    this.addConfigurers = this.addConfigurers.bind(this);
-    this.setMarkdownSettings = this.setMarkdownSettings.bind(this);
-    this.configure = this.configure.bind(this);
-    this.process = this.process.bind(this);
-    this.codeRenderer = this.codeRenderer.bind(this);
-  }
-
-  init() {
-    // init markdown-it
-    this.md = new MarkdownIt({
-      html: true,
-      linkify: true,
-      highlight: this.codeRenderer,
-    });
-
-    this.isMarkdownItConfigured = false;
-
-    this.markdownItConfigurers = [
-      new TaskListsConfigurer(),
-      new HeaderConfigurer(),
-      new EmojiConfigurer(),
-      new MathJaxConfigurer(this.growiRendererConfig),
-      new DrawioViewerConfigurer(),
-      new PlantUMLConfigurer(this.growiRendererConfig),
-      new BlockdiagConfigurer(this.growiRendererConfig),
-    ];
-
-    if (this.pagePath != null) {
-      this.markdownItConfigurers.push(
-        new LinkerByRelativePathConfigurer(this.pagePath),
-      );
-    }
-  }
+export const generateViewOptions: ReactMarkdownOptionsGenerator = (
+    growiRendererConfig: GrowiRendererConfig, rendererSettings: RendererSettings,
+): RendererOptions => {
 
-  addConfigurers(configurers: any[]): void {
-    this.markdownItConfigurers.push(...configurers);
-  }
+  const options = generateCommonOptions(growiRendererConfig, rendererSettings);
 
-  setMarkdownSettings(settings: MarkdownSettings): void {
-    this.md.set(settings);
-  }
-
-  configure(): void {
-    if (!this.isMarkdownItConfigured) {
-      this.markdownItConfigurers.forEach((configurer) => {
-        configurer.configure(this.md);
-      });
-    }
-  }
-
-  preProcess(markdown, context) {
-    let processed = markdown;
-    for (let i = 0; i < this.preProcessors.length; i++) {
-      if (!this.preProcessors[i].process) {
-        continue;
-      }
-      processed = this.preProcessors[i].process(processed, context);
-    }
-
-    return processed;
-  }
+  const { remarkPlugins, rehypePlugins } = options;
 
-  process(markdown, context) {
-    return this.md.render(markdown, context);
+  // add remark plugins
+  remarkPlugins?.push(footnotes);
+  remarkPlugins?.push(emoji);
+  if (rendererSettings.isEnabledLinebreaks) {
+    remarkPlugins?.push(breaks);
   }
-
-  postProcess(html, context) {
-    let processed = html;
-    for (let i = 0; i < this.postProcessors.length; i++) {
-      if (!this.postProcessors[i].process) {
-        continue;
-      }
-      processed = this.postProcessors[i].process(processed, context);
-    }
-
-    return processed;
-  }
-
-  codeRenderer(code, langExt) {
-    const noborder = (!this.growiRendererConfig.highlightJsStyleBorder) ? 'hljs-no-border' : '';
-
-    let citeTag = '';
-    let hljsLang = 'plaintext';
-    let showLinenumbers = false;
-
-    if (langExt) {
-      // https://regex101.com/r/qGs7eZ/3
-      const match = langExt.match(/^([^:=\n]+)?(=([^:=\n]*))?(:([^:=\n]*))?(=([^:=\n]*))?$/);
-
-      const lang = match[1];
-      const fileName = match[5] || null;
-      showLinenumbers = (match[2] != null) || (match[6] != null);
-
-      if (fileName != null) {
-        citeTag = `<cite>${fileName}</cite>`;
-      }
-      if (hljs.getLanguage(lang)) {
-        hljsLang = lang;
-      }
-    }
-
-    let highlightCode = code;
-    try {
-      highlightCode = hljs.highlight(hljsLang, code, true).value;
-
-      // add line numbers
-      if (showLinenumbers) {
-        highlightCode = hljs.lineNumbersValue((highlightCode));
-      }
-    }
-    catch (err) {
-      logger.error(err);
-    }
-
-    return `<pre class="hljs ${noborder}">${citeTag}<code>${highlightCode}</code></pre>`;
-  }
-
-}
-
-export interface RendererGenerator {
-  (growiRendererConfig: GrowiRendererConfig, rendererSettings: RendererSettings | null, pagePath?: Nullable<string>): GrowiRenderer
-}
-
-export const generateViewRenderer: RendererGenerator = (
-    growiRendererConfig: GrowiRendererConfig, rendererSettings: RendererSettings, pagePath?: Nullable<string>,
-): GrowiRenderer => {
-  const renderer = new GrowiRenderer(growiRendererConfig, pagePath);
-  renderer.init();
-
-  // Add configurers for viewer
-  renderer.addConfigurers([
-    new FooternoteConfigurer(),
-    new TocAndAnchorConfigurer(),
-    new HeaderLineNumberConfigurer(),
-    new HeaderWithEditLinkConfigurer(),
-    new TableWithHandsontableButtonConfigurer(),
-  ]);
-
-  renderer.setMarkdownSettings({ breaks: rendererSettings.isEnabledLinebreaks });
-  renderer.configure();
-
-  return renderer;
+  // add rehypePlugins
+  // rehypePlugins.push([toc, {
+  //   headings: ['h1', 'h2', 'h3'],
+  //   customizeTOC: storeTocNode,
+  // }]);
+  // renderer.rehypePlugins.push([autoLinkHeadings, {
+  //   behavior: 'append',
+  // }]);
+
+  // // Add configurers for viewer
+  // renderer.addConfigurers([
+  //   new FooternoteConfigurer(),
+  //   new TocAndAnchorConfigurer(),
+  //   new HeaderLineNumberConfigurer(),
+  //   new HeaderWithEditLinkConfigurer(),
+  //   new TableWithHandsontableButtonConfigurer(),
+  // ]);
+
+  // renderer.setMarkdownSettings({ breaks: rendererSettings.isEnabledLinebreaks });
+  // renderer.configure();
+
+  return options;
 };
 
-export const generatePreviewRenderer: RendererGenerator = (
-    growiRendererConfig: GrowiRendererConfig, rendererSettings: RendererSettings | null, pagePath?: Nullable<string>,
-): GrowiRenderer => {
-  const renderer = new GrowiRenderer(growiRendererConfig, pagePath);
-  renderer.init();
+export const generatePreviewOptions: ReactMarkdownOptionsGenerator = (
+    growiRendererConfig: GrowiRendererConfig, rendererSettings: RendererSettings,
+): RendererOptions => {
+  const options = generateCommonOptions(growiRendererConfig, rendererSettings);
 
-  // Add configurers for preview
-  renderer.addConfigurers([
-    new FooternoteConfigurer(),
-    new HeaderLineNumberConfigurer(),
-    new TableConfigurer(),
-  ]);
+  // // Add configurers for preview
+  // renderer.addConfigurers([
+  //   new FooternoteConfigurer(),
+  //   new HeaderLineNumberConfigurer(),
+  //   new TableConfigurer(),
+  // ]);
 
-  renderer.setMarkdownSettings({ breaks: rendererSettings?.isEnabledLinebreaks });
-  renderer.configure();
+  // renderer.setMarkdownSettings({ breaks: rendererSettings?.isEnabledLinebreaks });
+  // renderer.configure();
 
-  return renderer;
+  return options;
 };
 
-export const generateCommentPreviewRenderer: RendererGenerator = (
-    growiRendererConfig: GrowiRendererConfig, rendererSettings: RendererSettings, pagePath?: Nullable<string>,
-): GrowiRenderer => {
-  const renderer = new GrowiRenderer(growiRendererConfig, pagePath);
-  renderer.init();
+export const generateCommentPreviewOptions: ReactMarkdownOptionsGenerator = (
+    growiRendererConfig: GrowiRendererConfig, rendererSettings: RendererSettings,
+): RendererOptions => {
+  const options = generateCommonOptions(growiRendererConfig, rendererSettings);
 
-  renderer.addConfigurers([
-    new TableConfigurer(),
-  ]);
+  // renderer.addConfigurers([
+  //   new TableConfigurer(),
+  // ]);
 
-  renderer.setMarkdownSettings({ breaks: rendererSettings.isEnabledLinebreaksInComments });
-  renderer.configure();
+  // renderer.setMarkdownSettings({ breaks: rendererSettings.isEnabledLinebreaksInComments });
+  // renderer.configure();
 
-  return renderer;
+  return options;
 };
 
-export const generateOthersRenderer: RendererGenerator = (
-    growiRendererConfig: GrowiRendererConfig, rendererSettings: RendererSettings, pagePath?: Nullable<string>,
-): GrowiRenderer => {
-  const renderer = new GrowiRenderer(growiRendererConfig, pagePath);
-  renderer.init();
+export const generateOthersOptions: ReactMarkdownOptionsGenerator = (
+    growiRendererConfig: GrowiRendererConfig, rendererSettings: RendererSettings,
+): RendererOptions => {
+  const options = generateCommonOptions(growiRendererConfig, rendererSettings);
 
-  renderer.addConfigurers([
-    new TableConfigurer(),
-  ]);
+  // renderer.addConfigurers([
+  //   new TableConfigurer(),
+  // ]);
 
-  renderer.setMarkdownSettings({ breaks: rendererSettings.isEnabledLinebreaks });
-  renderer.configure();
+  // renderer.setMarkdownSettings({ breaks: rendererSettings.isEnabledLinebreaks });
+  // renderer.configure();
 
-  return renderer;
+  return options;
 };

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

+ 17 - 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);
 };
@@ -219,6 +230,10 @@ export const useGrowiRendererConfig = (initialData?: GrowiRendererConfig): SWRRe
   return useStaticSWR('growiRendererConfig', initialData);
 };
 
+export const useIsBlinkedHeaderAtBoot = (initialData?: boolean): SWRResponse<boolean, Error> => {
+  return useStaticSWR('isBlinkedAtBoot', initialData);
+};
+
 
 /** **********************************************************
  *                     Computed contexts
@@ -230,6 +245,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;
 };
 
 

+ 39 - 31
packages/app/src/stores/renderer.tsx

@@ -2,8 +2,9 @@ import { Key, SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 
 import { RendererSettings } from '~/interfaces/services/renderer';
-import GrowiRenderer, {
-  generateViewRenderer, generatePreviewRenderer, generateCommentPreviewRenderer, generateOthersRenderer, RendererGenerator,
+import {
+  ReactMarkdownOptionsGenerator, RendererOptions,
+  generateViewOptions, generatePreviewOptions, generateCommentPreviewOptions, generateOthersOptions,
 } from '~/services/renderer/growi-renderer';
 import { useStaticSWR } from '~/stores/use-static-swr';
 
@@ -14,57 +15,64 @@ export const useRendererSettings = (initialData?: RendererSettings): SWRResponse
 };
 
 // The base hook with common processes
-const _useRendererBase = (key: string, generator: RendererGenerator): SWRResponse<GrowiRenderer, Error> => {
+const _useOptionsBase = (rendererId: string, generator: ReactMarkdownOptionsGenerator): SWRResponse<RendererOptions, 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 && growiRendererConfig != null;
+
+  const key = isAllDataValid
+    ? [rendererId, rendererSettings, growiRendererConfig]
+    : null;
+
+  const swrResult = useSWRImmutable<RendererOptions, Error>(key);
+
+  if (isAllDataValid && swrResult.data == null) {
+    swrResult.mutate(generator(growiRendererConfig, rendererSettings));
+  }
+
+  // call useSWRImmutable again to foce to update cache
+  return useSWRImmutable<RendererOptions, Error>(key);
 };
 
-export const useViewRenderer = (): SWRResponse<GrowiRenderer, Error> => {
-  const key = 'viewRenderer';
+export const useViewOptions = (): SWRResponse<RendererOptions, Error> => {
+  const key = 'viewOptions';
 
-  return _useRendererBase(key, generateViewRenderer);
+  return _useOptionsBase(key, generateViewOptions);
 };
 
-export const usePreviewRenderer = (): SWRResponse<GrowiRenderer, Error> => {
-  const key = 'previewRenderer';
+export const usePreviewOptions = (): SWRResponse<RendererOptions, Error> => {
+  const key = 'previewOptions';
 
-  return _useRendererBase(key, generatePreviewRenderer);
+  return _useOptionsBase(key, generatePreviewOptions);
 };
 
-export const useCommentPreviewRenderer = (): SWRResponse<GrowiRenderer, Error> => {
-  const key = 'commentPreviewRenderer';
+export const useCommentPreviewOptions = (): SWRResponse<RendererOptions, Error> => {
+  const key = 'commentPreviewOptions';
 
-  return _useRendererBase(key, generateCommentPreviewRenderer);
+  return _useOptionsBase(key, generateCommentPreviewOptions);
 };
 
-export const useSearchResultRenderer = (): SWRResponse<GrowiRenderer, Error> => {
-  const key = 'searchResultRenderer';
+export const useSearchResultOptions = (): SWRResponse<RendererOptions, Error> => {
+  const key = 'searchResultOptions';
 
-  return _useRendererBase(key, generateOthersRenderer);
+  return _useOptionsBase(key, generateOthersOptions);
 };
 
-export const useTimelineRenderer = (): SWRResponse<GrowiRenderer, Error> => {
-  const key = 'timelineRenderer';
+export const useTimelineOptions = (): SWRResponse<RendererOptions, Error> => {
+  const key = 'timelineOptions';
 
-  return _useRendererBase(key, generateOthersRenderer);
+  return _useOptionsBase(key, generateOthersOptions);
 };
 
-export const useDraftRenderer = (): SWRResponse<GrowiRenderer, Error> => {
-  const key = 'draftRenderer';
+export const useDraftOptions = (): SWRResponse<RendererOptions, Error> => {
+  const key = 'draftOptions';
 
-  return _useRendererBase(key, generateOthersRenderer);
+  return _useOptionsBase(key, generateOthersOptions);
 };
 
-export const useCustomSidebarRenderer = (): SWRResponse<GrowiRenderer, Error> => {
-  const key: Key = 'customSidebarRenderer';
+export const useCustomSidebarOptions = (): SWRResponse<RendererOptions, Error> => {
+  const key: Key = 'customSidebarOptions';
 
-  return _useRendererBase(key, generateOthersRenderer);
+  return _useOptionsBase(key, generateOthersOptions);
 };

+ 21 - 0
packages/app/src/utils/next.config.utils.js

@@ -32,3 +32,24 @@ export const listScopedPackages = (scopes, opts = defaultOpts) => {
 
   return scopedPackages;
 };
+
+// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+export const listPrefixedPackages = (prefixes, opts = defaultOpts) => {
+  const prefixedPackages = [];
+
+  fs.readdirSync(nodeModulesPath)
+    .filter(name => prefixes.some(prefix => name.startsWith(prefix)))
+    .filter(name => !name.startsWith('.'))
+    .forEach((folderName) => {
+      const { name, ignoreTranspileModules } = require(path.resolve(
+        nodeModulesPath,
+        folderName,
+        'package.json',
+      ));
+      if (!ignoreTranspileModules && !opts.ignorePackageNames.includes(name)) {
+        prefixedPackages.push(name);
+      }
+    });
+
+  return prefixedPackages;
+};

+ 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": [

Plik diff jest za duży
+ 572 - 90
yarn.lock


Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików