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

Merge pull request #6002 from weseek/master

Release v5.0.9
Yuki Takei 3 лет назад
Родитель
Сommit
6c01f4467a
100 измененных файлов с 804 добавлено и 617 удалено
  1. 26 0
      CHANGELOG.md
  2. 1 1
      lerna.json
  3. 1 1
      package.json
  4. 1 0
      packages/app/config/webpack.common.js
  5. 2 2
      packages/app/docker/README.md
  6. 7 7
      packages/app/package.json
  7. 1 0
      packages/app/resource/locales/en_US/translation.json
  8. 1 0
      packages/app/resource/locales/ja_JP/translation.json
  9. 2 1
      packages/app/resource/locales/zh_CN/translation.json
  10. 2 0
      packages/app/src/client/base.jsx
  11. 60 0
      packages/app/src/client/installer.jsx
  12. 2 1
      packages/app/src/client/legacy/crowi.js
  13. 67 65
      packages/app/src/client/nologin.jsx
  14. 0 15
      packages/app/src/client/services/AppContainer.js
  15. 2 3
      packages/app/src/client/services/CommentContainer.js
  16. 10 2
      packages/app/src/client/services/ContextExtractor.tsx
  17. 5 3
      packages/app/src/client/services/PageContainer.js
  18. 7 0
      packages/app/src/client/util/apiv1-client.ts
  19. 12 3
      packages/app/src/client/util/apiv3-client.ts
  20. 2 1
      packages/app/src/client/util/markdown-it/toc-and-anchor.js
  21. 2 2
      packages/app/src/client/util/reveal/plugins/growi-renderer.js
  22. 8 8
      packages/app/src/components/Admin/App/AwsSetting.jsx
  23. 8 3
      packages/app/src/components/Admin/ExportArchiveDataPage.jsx
  24. 0 33
      packages/app/src/components/Admin/FullTextSearchManagement.jsx
  25. 22 0
      packages/app/src/components/Admin/FullTextSearchManagement.tsx
  26. 3 13
      packages/app/src/components/Admin/ImportData/GrowiArchive/UploadForm.jsx
  27. 13 7
      packages/app/src/components/Admin/ManageExternalAccount.jsx
  28. 9 6
      packages/app/src/components/ArchiveCreateModal.jsx
  29. 9 6
      packages/app/src/components/Common/CountBadge.tsx
  30. 4 6
      packages/app/src/components/CreateTemplateModal.jsx
  31. 6 9
      packages/app/src/components/Drawio.tsx
  32. 1 1
      packages/app/src/components/FormattedDistanceDate.jsx
  33. 9 6
      packages/app/src/components/InAppNotification/InAppNotificationDropdown.tsx
  34. 5 4
      packages/app/src/components/InAppNotification/PageNotification/PageModelNotification.tsx
  35. 13 5
      packages/app/src/components/InstallerForm.jsx
  36. 24 13
      packages/app/src/components/LoginForm.jsx
  37. 12 4
      packages/app/src/components/Me/ApiSettings.jsx
  38. 11 4
      packages/app/src/components/Me/AssociateModal.jsx
  39. 12 4
      packages/app/src/components/Me/BasicInfoSettings.jsx
  40. 17 7
      packages/app/src/components/Me/DisassociateModal.jsx
  41. 12 4
      packages/app/src/components/Me/ExternalAccountLinkedMe.jsx
  42. 5 7
      packages/app/src/components/Me/ExternalAccountRow.jsx
  43. 7 5
      packages/app/src/components/Me/ImageCropModal.jsx
  44. 12 5
      packages/app/src/components/Me/PasswordSettings.jsx
  45. 4 8
      packages/app/src/components/Me/PersonalSettings.jsx
  46. 5 16
      packages/app/src/components/Me/ProfileImageSettings.tsx
  47. 0 36
      packages/app/src/components/Me/UserSettings.jsx
  48. 29 0
      packages/app/src/components/Me/UserSettings.tsx
  49. 15 12
      packages/app/src/components/MyDraftList/Draft.jsx
  50. 12 8
      packages/app/src/components/MyDraftList/MyDraftList.jsx
  51. 1 4
      packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  52. 7 10
      packages/app/src/components/Page/CopyDropdown.jsx
  53. 2 2
      packages/app/src/components/Page/DisplaySwitcher.tsx
  54. 6 1
      packages/app/src/components/Page/RevisionBody.jsx
  55. 2 2
      packages/app/src/components/Page/RevisionRenderer.jsx
  56. 4 11
      packages/app/src/components/Page/ShareLinkAlert.jsx
  57. 8 9
      packages/app/src/components/Page/TrashPageAlert.jsx
  58. 15 16
      packages/app/src/components/PageAccessoriesModalControl.jsx
  59. 2 2
      packages/app/src/components/PageComment/Comment.jsx
  60. 5 6
      packages/app/src/components/PageComment/CommentEditor.jsx
  61. 15 20
      packages/app/src/components/PageComment/CommentPreview.jsx
  62. 8 10
      packages/app/src/components/PageCreateModal.jsx
  63. 9 12
      packages/app/src/components/PageEditor.tsx
  64. 8 2
      packages/app/src/components/PageEditor/Cheatsheet.jsx
  65. 5 2
      packages/app/src/components/PageEditor/Preview.tsx
  66. 8 2
      packages/app/src/components/PageEditor/SimpleCheatsheet.jsx
  67. 20 18
      packages/app/src/components/PageEditorByHackmd.jsx
  68. 8 2
      packages/app/src/components/PageHistory/PageRevisionTable.jsx
  69. 10 3
      packages/app/src/components/PageHistory/RevisionDiff.jsx
  70. 5 6
      packages/app/src/components/PageManagement/ApiErrorMessage.jsx
  71. 12 7
      packages/app/src/components/PageStatusAlert.jsx
  72. 12 7
      packages/app/src/components/PageTimeline.jsx
  73. 3 11
      packages/app/src/components/PasswordResetExecutionForm.jsx
  74. 3 14
      packages/app/src/components/PasswordResetRequestForm.jsx
  75. 9 10
      packages/app/src/components/RevisionComparer/RevisionComparer.jsx
  76. 4 2
      packages/app/src/components/SavePageControls.jsx
  77. 12 7
      packages/app/src/components/ShareLink/ShareLink.jsx
  78. 12 7
      packages/app/src/components/ShareLink/ShareLinkForm.jsx
  79. 4 9
      packages/app/src/components/TrashPageList.jsx
  80. 1 1
      packages/app/src/interfaces/page.ts
  81. 0 6
      packages/app/src/server/crowi/index.js
  82. 6 0
      packages/app/src/server/models/errors.ts
  83. 5 0
      packages/app/src/server/models/external-account.js
  84. 2 1
      packages/app/src/server/routes/apiv3/page.js
  85. 0 1
      packages/app/src/server/routes/index.js
  86. 7 1
      packages/app/src/server/routes/login-passport.js
  87. 0 4
      packages/app/src/server/routes/page.js
  88. 8 7
      packages/app/src/server/service/page-grant.ts
  89. 18 16
      packages/app/src/server/service/page.ts
  90. 4 3
      packages/app/src/server/views/installer.html
  91. 4 0
      packages/app/src/stores/context.tsx
  92. 11 7
      packages/app/src/stores/websocket.tsx
  93. 6 0
      packages/app/src/styles/_navbar.scss
  94. 5 1
      packages/app/src/styles/theme/_apply-colors-light.scss
  95. 1 1
      packages/app/src/styles/theme/blackboard.scss
  96. 1 1
      packages/app/src/styles/theme/christmas.scss
  97. 2 2
      packages/app/src/styles/theme/fire-red.scss
  98. 2 2
      packages/app/src/styles/theme/future.scss
  99. 1 1
      packages/app/src/styles/theme/halloween.scss
  100. 1 1
      packages/app/src/styles/theme/hufflepuff.scss

+ 26 - 0
CHANGELOG.md

@@ -30,6 +30,18 @@
 - support: Enable garbage collection at runtime with expose-gc package (#5986) @yuki-takei
 - support: Upgrade aws-sdk to v3 (#5863) @mudana-grune
 
+## [v4.5.22](https://github.com/weseek/growi/compare/v4.5.21...v4.5.22) - 2022-06-07
+
+### 🐛 Bug Fixes
+
+- fix: Fixed the bug of auto-filling unintended values into the Email field of the User settings (#5885) @Shunm634-source
+- fix: google-oauth2 Automatically bind external accounts does not work (#5891) @kaoritokashiki
+- fix: Slack channels are not automatically filled after setting up user trigger notification (#5976) @kaoritokashiki
+
+### 🧰 Maintenance
+
+- support: Enable garbage collection at runtime with expose-gc package (#5998) @kaoritokashiki
+
 ## [v5.0.7](https://github.com/weseek/growi/compare/v5.0.6...v5.0.7) - 2022-05-30
 
 ### 💎 Features
@@ -69,6 +81,13 @@
 - fix: Can not toggle textlint function on v5.0.x (#5854) @kaoritokashiki
 - fix(google-oauth2): Automatically bind external accounts  does not work on v5.0.x (#5886) @kaoritokashiki
 
+## [v4.5.21](https://github.com/weseek/growi/compare/v4.5.20...v4.5.21) - 2022-05-23
+
+### 🐛 Bug Fixes
+
+- fix: Can not toggle textlint function on v4.5.x (https://github.com/weseek/growi/pull/5855) @kaoritokashiki
+- fix: Error on searching (https://github.com/weseek/growi/pull/5873) @miya
+
 ## [v5.0.5](https://github.com/weseek/growi/compare/v5.0.4...v5.0.5) - 2022-05-16
 
 ### 💎 Features
@@ -93,6 +112,13 @@
 
 - support: Typescriptize tag model (#5778) @kaoritokashiki
 
+
+## [v4.5.20](https://github.com/weseek/growi/compare/v4.5.19...v4.5.20) - 2022-05-12
+
+### 🐛 Bug Fixes
+
+- fix: Guest user cannot access share link pages (#5819) @kaoritokashiki
+
 ## [v5.0.4](https://github.com/weseek/growi/compare/v5.0.3...v5.0.4) - 2022-04-28
 
 ### 💎 Features

+ 1 - 1
lerna.json

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

+ 1 - 1
package.json

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

+ 1 - 0
packages/app/config/webpack.common.js

@@ -25,6 +25,7 @@ module.exports = (options) => {
       'js/app':                       './src/client/app',
       'js/admin':                     './src/client/admin',
       'js/nologin':                   './src/client/nologin',
+      'js/installer':                   './src/client/installer',
       'js/legacy':                    './src/client/legacy/crowi',
       'js/legacy-presentation':       './src/client/legacy/crowi-presentation',
       'js/plugin':                    './src/client/plugin',

+ 2 - 2
packages/app/docker/README.md

@@ -12,8 +12,8 @@ Supported tags and respective Dockerfile links
 
 * [`5.0.8`, `5.0`, `5`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.8/docker/Dockerfile)
 * [`5.0.8-nocdn`, `5.0-nocdn`, `5-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.8/docker/Dockerfile)
-* [`4.5.15`, `4.5`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.15/docker/Dockerfile)
-* [`4.5.15-nocdn`, `4.5-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.15/docker/Dockerfile)
+* [`4.5.22`, `4.5`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.22/docker/Dockerfile)
+* [`4.5.22-nocdn`, `4.5-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.22/docker/Dockerfile)
 * [`4.4.13`, `4.4` (Dockerfile)](https://github.com/weseek/growi/blob/v4.4.13/docker/Dockerfile)
 * [`4.4.13-nocdn`, `4.4-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.4.13/docker/Dockerfile)
 

+ 7 - 7
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "5.0.8",
+  "version": "5.0.9-RC.0",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -64,11 +64,11 @@
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^5.0.8",
-    "@growi/plugin-attachment-refs": "^5.0.8",
-    "@growi/plugin-lsx": "^5.0.8",
-    "@growi/plugin-pukiwiki-like-linker": "^5.0.8",
-    "@growi/slack": "^5.0.8",
+    "@growi/codemirror-textlint": "^5.0.9-RC.0",
+    "@growi/plugin-attachment-refs": "^5.0.9-RC.0",
+    "@growi/plugin-lsx": "^5.0.9-RC.0",
+    "@growi/plugin-pukiwiki-like-linker": "^5.0.9-RC.0",
+    "@growi/slack": "^5.0.9-RC.0",
     "@promster/express": "^7.0.2",
     "@promster/server": "^7.0.4",
     "@slack/events-api": "^3.0.0",
@@ -169,7 +169,7 @@
   },
   "devDependencies": {
     "@alienfast/i18next-loader": "^1.1.4",
-    "@growi/ui": "^5.0.8",
+    "@growi/ui": "^5.0.9-RC.0",
     "@handsontable/react": "=2.1.0",
     "@types/compression": "^1.7.0",
     "@types/express": "^4.17.11",

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

@@ -984,6 +984,7 @@
     "application_already_installed": "Application already installed.",
     "email_address_could_not_be_used": "This email address could not be used. (Make sure the allowed email address)",
     "user_id_is_not_available":"This User ID is not available.",
+    "username_should_not_be_null":"Username should not be null. Please check Authentication Mechanism Settings on admin page",
     "email_address_is_already_registered":"This email address is already registered.",
     "can_not_register_maximum_number_of_users":"Can not register more than the maximum number of users.",
     "failed_to_register":"Failed to register.",

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

@@ -977,6 +977,7 @@
     "application_already_installed": "アプリケーションのインストールが完了しました。",
     "email_address_could_not_be_used":"このメールアドレスは使用できません。(許可されたメールアドレスを確認してください。)",
     "user_id_is_not_available":"このユーザーIDは使用できません。",
+    "username_should_not_be_null":"Username が null になっています 管理画面の認証機構設定にて設定の確認をしてください",
     "email_address_is_already_registered":"このメールアドレスは既に登録されています。",
     "can_not_register_maximum_number_of_users":"ユーザー数が上限を超えたため登録できません。",
     "failed_to_register":"登録に失敗しました。",

+ 2 - 1
packages/app/resource/locales/zh_CN/translation.json

@@ -986,7 +986,8 @@
 		"aws_sttings_required": "使用此功能所需的AWS设置。请询问管理员。",
 		"application_already_installed": "应用程序已安装。",
 		"email_address_could_not_be_used": "无法使用此电子邮件地址。(确保允许的电子邮件地址)",
-		"user_id_is_not_available": "此用户ID不可用。",
+    "user_id_is_not_available": "此用户ID不可用。",
+    "username_should_not_be_null":"用户名不应为空。请检查管理页面上的身份验证机制设置",
 		"email_address_is_already_registered": "此电子邮件地址已注册。",
 		"can_not_register_maximum_number_of_users": "注册的用户数不能超过最大值。",
 		"failed_to_register": "注册失败。",

+ 2 - 0
packages/app/src/client/base.jsx

@@ -6,6 +6,7 @@ import AppContainer from '~/client/services/AppContainer';
 import SocketIoContainer from '~/client/services/SocketIoContainer';
 import { DescendantsPageListModal } from '~/components/DescendantsPageListModal';
 import PutbackPageModal from '~/components/PutbackPageModal';
+import InterceptorManager from '~/services/interceptor-manager';
 import Xss from '~/services/xss';
 import loggerFactory from '~/utils/logger';
 
@@ -31,6 +32,7 @@ const xss = new Xss();
 window.xss = xss;
 
 window.globalEmitter = new EventEmitter();
+window.interceptorManager = new InterceptorManager();
 
 // create unstated container instance
 const appContainer = new AppContainer();

+ 60 - 0
packages/app/src/client/installer.jsx

@@ -0,0 +1,60 @@
+import React from 'react';
+
+import ReactDOM from 'react-dom';
+import { I18nextProvider } from 'react-i18next';
+import { SWRConfig } from 'swr';
+
+
+import { swrGlobalConfiguration } from '~/utils/swr-utils';
+
+import InstallerForm from '../components/InstallerForm';
+
+import ContextExtractor from './services/ContextExtractor';
+import { i18nFactory } from './util/i18n';
+
+const i18n = i18nFactory();
+
+const componentMappings = {};
+
+// render InstallerForm
+const installerFormContainerElem = document.getElementById('installer-form-container');
+if (installerFormContainerElem) {
+  const userName = installerFormContainerElem.dataset.userName;
+  const name = installerFormContainerElem.dataset.name;
+  const email = installerFormContainerElem.dataset.email;
+
+  Object.assign(componentMappings, {
+    'installer-form-container': <InstallerForm userName={userName} name={name} email={email} />,
+  });
+}
+
+const renderMainComponents = () => {
+  Object.keys(componentMappings).forEach((key) => {
+    const elem = document.getElementById(key);
+    if (elem) {
+      ReactDOM.render(
+        <I18nextProvider i18n={i18n}>
+          <SWRConfig value={swrGlobalConfiguration}>
+            {componentMappings[key]}
+          </SWRConfig>
+        </I18nextProvider>,
+        elem,
+      );
+    }
+  });
+};
+
+// extract context before rendering main components
+const elem = document.getElementById('growi-context-extractor');
+if (elem != null) {
+  ReactDOM.render(
+    <SWRConfig value={swrGlobalConfiguration}>
+      <ContextExtractor></ContextExtractor>
+    </SWRConfig>,
+    elem,
+    renderMainComponents,
+  );
+}
+else {
+  renderMainComponents();
+}

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

@@ -13,7 +13,8 @@ if (!window) {
 window.Crowi = Crowi;
 
 Crowi.setCaretLine = function(line) {
-  window.globalEmitter.emit('setCaretLine', line);
+  // eslint-disable-next-line no-undef
+  globalEmitter.emit('setCaretLine', line);
 };
 
 // original: middleware.swigFilter

+ 67 - 65
packages/app/src/client/nologin.jsx

@@ -2,42 +2,32 @@ import React from 'react';
 
 import ReactDOM from 'react-dom';
 import { I18nextProvider } from 'react-i18next';
+import { SWRConfig } from 'swr';
 import { Provider } from 'unstated';
 
 
 import AppContainer from '~/client/services/AppContainer';
 import CompleteUserRegistrationForm from '~/components/CompleteUserRegistrationForm';
+import { swrGlobalConfiguration } from '~/utils/swr-utils';
 
-import InstallerForm from '../components/InstallerForm';
 import LoginForm from '../components/LoginForm';
 import PasswordResetExecutionForm from '../components/PasswordResetExecutionForm';
 import PasswordResetRequestForm from '../components/PasswordResetRequestForm';
 
+import ContextExtractor from './services/ContextExtractor';
 import { i18nFactory } from './util/i18n';
 
 const i18n = i18nFactory();
 
-// render InstallerForm
-const installerFormContainerElem = document.getElementById('installer-form-container');
-if (installerFormContainerElem) {
-  const userName = installerFormContainerElem.dataset.userName;
-  const name = installerFormContainerElem.dataset.name;
-  const email = installerFormContainerElem.dataset.email;
-  const csrf = installerFormContainerElem.dataset.csrf;
-  ReactDOM.render(
-    <I18nextProvider i18n={i18n}>
-      <InstallerForm userName={userName} name={name} email={email} csrf={csrf} />
-    </I18nextProvider>,
-    installerFormContainerElem,
-  );
-}
+
+const componentMappings = {};
+
+const appContainer = new AppContainer();
+appContainer.initApp();
 
 // render loginForm
 const loginFormElem = document.getElementById('login-form');
 if (loginFormElem) {
-  const appContainer = new AppContainer();
-  appContainer.initApp();
-
   const username = loginFormElem.dataset.username;
   const name = loginFormElem.dataset.name;
   const email = loginFormElem.dataset.email;
@@ -65,78 +55,90 @@ if (loginFormElem) {
     basic: loginFormElem.dataset.isBasicAuthEnabled === 'true',
   };
 
-  ReactDOM.render(
-    <I18nextProvider i18n={i18n}>
-      <Provider inject={[appContainer]}>
-        <LoginForm
-          username={username}
-          name={name}
-          email={email}
-          isRegistrationEnabled={isRegistrationEnabled}
-          isEmailAuthenticationEnabled={isEmailAuthenticationEnabled}
-          registrationMode={registrationMode}
-          registrationWhiteList={registrationWhiteList}
-          isPasswordResetEnabled={isPasswordResetEnabled}
-          isLocalStrategySetup={isLocalStrategySetup}
-          isLdapStrategySetup={isLdapStrategySetup}
-          objOfIsExternalAuthEnableds={objOfIsExternalAuthEnableds}
-        />
-      </Provider>
-    </I18nextProvider>,
-    loginFormElem,
-  );
+  Object.assign(componentMappings, {
+    [loginFormElem.id]: (
+      <LoginForm
+        username={username}
+        name={name}
+        email={email}
+        isRegistrationEnabled={isRegistrationEnabled}
+        isEmailAuthenticationEnabled={isEmailAuthenticationEnabled}
+        registrationMode={registrationMode}
+        registrationWhiteList={registrationWhiteList}
+        isPasswordResetEnabled={isPasswordResetEnabled}
+        isLocalStrategySetup={isLocalStrategySetup}
+        isLdapStrategySetup={isLdapStrategySetup}
+        objOfIsExternalAuthEnableds={objOfIsExternalAuthEnableds}
+      />
+    ),
+  });
 }
 
-const appContainer = new AppContainer();
-appContainer.initApp();
-
-
 // render PasswordResetRequestForm
 const passwordResetRequestFormElem = document.getElementById('password-reset-request-form');
 if (passwordResetRequestFormElem) {
-
-  ReactDOM.render(
-    <I18nextProvider i18n={i18n}>
-      <Provider inject={[appContainer]}>
-        <PasswordResetRequestForm />
-      </Provider>
-    </I18nextProvider>,
-    passwordResetRequestFormElem,
-  );
+  Object.assign(componentMappings, {
+    [passwordResetRequestFormElem.id]: <PasswordResetRequestForm />,
+  });
 }
 
 // render PasswordResetExecutionForm
 const passwordResetExecutionFormElem = document.getElementById('password-reset-execution-form');
 if (passwordResetExecutionFormElem) {
-
-  ReactDOM.render(
-    <I18nextProvider i18n={i18n}>
-      <Provider inject={[appContainer]}>
-        <PasswordResetExecutionForm />
-      </Provider>
-    </I18nextProvider>,
-    passwordResetExecutionFormElem,
-  );
+  Object.assign(componentMappings, {
+    [passwordResetExecutionFormElem.id]: <PasswordResetExecutionForm />,
+  });
 }
 
 // render UserActivationForm
 const UserActivationForm = document.getElementById('user-activation-form');
 if (UserActivationForm) {
-
   const messageErrors = UserActivationForm.dataset.messageErrors;
   const inputs = UserActivationForm.dataset.inputs;
   const email = UserActivationForm.dataset.email;
   const token = UserActivationForm.dataset.token;
 
-  ReactDOM.render(
-    <I18nextProvider i18n={i18n}>
+  Object.assign(componentMappings, {
+    [UserActivationForm.id]: (
       <CompleteUserRegistrationForm
         messageErrors={messageErrors}
         inputs={inputs}
         email={email}
         token={token}
       />
-    </I18nextProvider>,
-    UserActivationForm,
+    ),
+  });
+}
+
+const renderMainComponents = () => {
+  Object.keys(componentMappings).forEach((key) => {
+    const elem = document.getElementById(key);
+    if (elem) {
+      ReactDOM.render(
+        <I18nextProvider i18n={i18n}>
+          <SWRConfig value={swrGlobalConfiguration}>
+            <Provider inject={[appContainer]}>
+              {componentMappings[key]}
+            </Provider>
+          </SWRConfig>
+        </I18nextProvider>,
+        elem,
+      );
+    }
+  });
+};
+
+// extract context before rendering main components
+const elem = document.getElementById('growi-context-extractor');
+if (elem != null) {
+  ReactDOM.render(
+    <SWRConfig value={swrGlobalConfiguration}>
+      <ContextExtractor></ContextExtractor>
+    </SWRConfig>,
+    elem,
+    renderMainComponents,
   );
 }
+else {
+  renderMainComponents();
+}

+ 0 - 15
packages/app/src/client/services/AppContainer.js

@@ -1,6 +1,5 @@
 import { Container } from 'unstated';
 
-import InterceptorManager from '~/services/interceptor-manager';
 
 import GrowiRenderer from '../util/GrowiRenderer';
 import { i18nFactory } from '../util/i18n';
@@ -14,11 +13,6 @@ export default class AppContainer extends Container {
   constructor() {
     super();
 
-    // get csrf token from body element
-    // DO NOT REMOVE: uploading attachment data requires appContainer.csrfToken
-    const body = document.querySelector('body');
-    this.csrfToken = body.dataset.csrftoken;
-
     this.config = JSON.parse(document.getElementById('growi-context-hydrate').textContent || '{}');
 
     const currentUserElem = document.getElementById('growi-current-user');
@@ -52,8 +46,6 @@ export default class AppContainer extends Container {
 
     this.originRenderer = new GrowiRenderer(this);
 
-    this.interceptorManager = new InterceptorManager();
-
     const isPluginEnabled = body.dataset.pluginEnabled === 'true';
     if (isPluginEnabled) {
       this.initPlugins();
@@ -93,13 +85,6 @@ export default class AppContainer extends Container {
     return this.currentUser.username;
   }
 
-  /**
-   * @return {Object} window.Crowi (js/legacy/crowi.js)
-   */
-  getCrowiForJquery() {
-    return window.Crowi;
-  }
-
   getConfig() {
     return this.config;
   }

+ 2 - 3
packages/app/src/client/services/CommentContainer.js

@@ -2,7 +2,7 @@ import { Container } from 'unstated';
 
 import loggerFactory from '~/utils/logger';
 
-import { apiGet, apiPost } from '../util/apiv1-client';
+import { apiGet, apiPost, apiPostForm } from '../util/apiv1-client';
 import { apiv3Put } from '../util/apiv3-client';
 
 const logger = loggerFactory('growi:services:CommentContainer');
@@ -157,12 +157,11 @@ export default class CommentContainer extends Container {
 
     const endpoint = '/attachments.add';
     const formData = new FormData();
-    formData.append('_csrf', this.appContainer.csrfToken);
     formData.append('file', file);
     formData.append('path', pagePath);
     formData.append('page_id', pageId);
 
-    return apiPost(endpoint, formData);
+    return apiPostForm(endpoint, formData);
   }
 
 }

+ 10 - 2
packages/app/src/client/services/ContextExtractor.tsx

@@ -18,7 +18,7 @@ import {
   useShareLinkId, useShareLinksNumber, useTemplateTagData, useCurrentUpdatedAt, useCreator, useRevisionAuthor, useCurrentUser, useTargetAndAncestors,
   useNotFoundTargetPathOrId, useIsSearchPage, useIsForbidden, useIsIdenticalPath, useHasParent,
   useIsAclEnabled, useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsEnabledAttachTitleHeader, useIsNotFoundPermalink,
-  useDefaultIndentSize, useIsIndentSizeForced,
+  useDefaultIndentSize, useIsIndentSizeForced, useCsrfToken,
 } from '../../stores/context';
 
 const { isTrashPage: _isTrashPage } = pagePathUtils;
@@ -32,6 +32,11 @@ const ContextExtractorOnce: FC = () => {
   const notFoundContent = document.getElementById('growi-not-found-context');
   const forbiddenContent = document.getElementById('forbidden-page');
 
+  // get csrf token from body element
+  // DO NOT REMOVE: uploading attachment data requires appContainer.csrfToken
+  const body = document.querySelector('body');
+  const csrfToken = body?.dataset.csrftoken;
+
   /*
    * App Context from DOM
    */
@@ -94,6 +99,8 @@ const ContextExtractorOnce: FC = () => {
   /*
    * use static swr
    */
+  useCsrfToken(csrfToken);
+
   // App
   useCurrentUser(currentUser);
 
@@ -166,7 +173,8 @@ const ContextExtractorOnce: FC = () => {
 
   // Global Socket
   useSetupGlobalSocket();
-  useSetupGlobalAdminSocket();
+  const shouldInitAdminSock = !!currentUser?.isAdmin;
+  useSetupGlobalAdminSocket(shouldInitAdminSock);
 
   return null;
 };

+ 5 - 3
packages/app/src/client/services/PageContainer.js

@@ -99,7 +99,7 @@ export default class PageContainer extends Container {
       logger.warn('The data of \'data-page-revision-author\' is invalid', e);
     }
 
-    const { interceptorManager } = this.appContainer;
+    const { interceptorManager } = window;
     interceptorManager.addInterceptor(new DetachCodeBlockInterceptor(), 10); // process as soon as possible
     interceptorManager.addInterceptor(new DrawioInterceptor(), 20);
     interceptorManager.addInterceptor(new RestoreCodeBlockInterceptor(), 900); // process as late as possible
@@ -219,7 +219,8 @@ export default class PageContainer extends Container {
 
     // Update PageEditor component
     if (editorMode !== EditorMode.Editor) {
-      window.globalEmitter.emit('updateEditorValue', newState.markdown);
+      // eslint-disable-next-line no-undef
+      globalEmitter.emit('updateEditorValue', newState.markdown);
     }
 
     // PageEditorByHackmd component
@@ -459,7 +460,8 @@ export default class PageContainer extends Container {
 
     // Update PageEditor component
     if (editorMode !== EditorMode.Editor) {
-      window.globalEmitter.emit('updateEditorValue', markdown);
+      // eslint-disable-next-line no-undef
+      globalEmitter.emit('updateEditorValue', markdown);
     }
 
     editorContainer.setState({ tags: res.tags });

+ 7 - 0
packages/app/src/client/util/apiv1-client.ts

@@ -58,6 +58,13 @@ export async function apiPost(path: string, params: any & ParamWithCsrfKey = {})
   return apiRequest('post', path, params);
 }
 
+export async function apiPostForm(path: string, formData: FormData): Promise<unknown> {
+  if (formData.get('_csrf') == null && csrfToken != null) {
+    formData.append('_csrf', csrfToken);
+  }
+  return apiPost(path, formData);
+}
+
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
 export async function apiDelete(path: string, params: any & ParamWithCsrfKey = {}): Promise<unknown> {
   if (params._csrf == null) {

+ 12 - 3
packages/app/src/client/util/apiv3-client.ts

@@ -1,11 +1,12 @@
+// eslint-disable-next-line no-restricted-imports
+import { AxiosResponse } from 'axios';
 import * as urljoin from 'url-join';
 
 // eslint-disable-next-line no-restricted-imports
-import { AxiosResponse } from 'axios';
 
-import loggerFactory from '~/utils/logger';
-import axios from '~/utils/axios';
 import { toArrayIfNot } from '~/utils/array-utils';
+import axios from '~/utils/axios';
+import loggerFactory from '~/utils/logger';
 
 const apiv3Root = '/_api/v3';
 
@@ -57,6 +58,14 @@ export async function apiv3Post<T = any>(path: string, params: any & ParamWithCs
   return apiv3Request('post', path, params);
 }
 
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export async function apiv3PostForm<T = any>(path: string, formData: FormData): Promise<AxiosResponse<T>> {
+  if (formData.get('_csrf') == null && csrfToken != null) {
+    formData.append('_csrf', csrfToken);
+  }
+  return apiv3Post<T>(path, formData);
+}
+
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
 export async function apiv3Put<T = any>(path: string, params: any & ParamWithCsrfKey = {}): Promise<AxiosResponse<T>> {
   if (params._csrf == null) {

+ 2 - 1
packages/app/src/client/util/markdown-it/toc-and-anchor.js

@@ -18,7 +18,8 @@ export default class TocAndAnchorConfigurer {
     // set toc render function
     md.set({
       tocCallback: (tocMarkdown, tocArray, tocHtml) => {
-        window.globalEmitter.emit('renderTocHtml', tocHtml);
+        // eslint-disable-next-line no-undef
+        globalEmitter.emit('renderTocHtml', tocHtml);
       },
     });
   }

+ 2 - 2
packages/app/src/client/util/reveal/plugins/growi-renderer.js

@@ -28,7 +28,7 @@
       const section = sections[i];
       const markdown = marked.getMarkdownFromSlide(section);
       const context = { markdown };
-      const interceptorManager = appContainer.interceptorManager;
+      const { interceptorManager } = window.parent;
       let dataSeparator = section.getAttribute('data-separator') || DEFAULT_SLIDE_SEPARATOR;
       // replace string '\n' to LF code.
       dataSeparator = dataSeparator.replace(/\\n/g, '\n');
@@ -51,7 +51,7 @@
   function convertSlides() {
     const sections = document.querySelectorAll('[data-markdown]');
     let markdown;
-    const interceptorManager = appContainer.interceptorManager;
+    const { interceptorManager } = window.parent;
 
     for (let i = 0, len = sections.length; i < len; i++) {
       const section = sections[i];

+ 8 - 8
packages/app/src/components/Admin/App/AwsSetting.jsx

@@ -1,14 +1,16 @@
 import React from 'react';
+
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
+
+import AdminAppContainer from '~/client/services/AdminAppContainer';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
-import AppContainer from '~/client/services/AppContainer';
-import AdminAppContainer from '~/client/services/AdminAppContainer';
 
 function AwsSetting(props) {
-  const { t, adminAppContainer } = props;
+  const { t } = useTranslation();
+  const { adminAppContainer } = props;
   const { s3ReferenceFileWithRelayMode } = adminAppContainer.state;
 
   return (
@@ -150,12 +152,10 @@ function AwsSetting(props) {
 /**
  * Wrapper component for using unstated
  */
-const AwsSettingWrapper = withUnstatedContainers(AwsSetting, [AppContainer, AdminAppContainer]);
+const AwsSettingWrapper = withUnstatedContainers(AwsSetting, [AdminAppContainer]);
 
 AwsSetting.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminAppContainer: PropTypes.instanceOf(AdminAppContainer).isRequired,
 };
 
-export default withTranslation()(AwsSettingWrapper);
+export default AwsSettingWrapper;

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

@@ -1,7 +1,7 @@
 import React, { Fragment } from 'react';
 
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 import * as toastr from 'toastr';
 
 
@@ -254,9 +254,14 @@ ExportArchiveDataPage.propTypes = {
   adminSocketIoContainer: PropTypes.instanceOf(AdminSocketIoContainer).isRequired,
 };
 
+const ExportArchiveDataPageWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <ExportArchiveDataPage t={t} {...props} />;
+};
+
 /**
  * Wrapper component for using unstated
  */
-const ExportArchiveDataPageWrapper = withUnstatedContainers(ExportArchiveDataPage, [AppContainer, AdminSocketIoContainer]);
+const ExportArchiveDataPageWrapper = withUnstatedContainers(ExportArchiveDataPageWrapperFC, [AppContainer, AdminSocketIoContainer]);
 
-export default withTranslation()(ExportArchiveDataPageWrapper);
+export default ExportArchiveDataPageWrapper;

+ 0 - 33
packages/app/src/components/Admin/FullTextSearchManagement.jsx

@@ -1,33 +0,0 @@
-import React, { Fragment } from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import { withUnstatedContainers } from '../UnstatedUtils';
-import AppContainer from '~/client/services/AppContainer';
-
-import ElasticsearchManagement from './ElasticsearchManagement/ElasticsearchManagement';
-
-
-class FullTextSearchManagement extends React.Component {
-
-  render() {
-    const { t } = this.props;
-
-    return (
-      <div data-testid="admin-full-text-search">
-        <h2> { t('full_text_search_management.elasticsearch_management') } </h2>
-        <ElasticsearchManagement />
-      </div>
-    );
-  }
-
-}
-
-const FullTextSearchManagementWrapper = withUnstatedContainers(FullTextSearchManagement, [AppContainer]);
-
-FullTextSearchManagement.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-};
-
-export default withTranslation()(FullTextSearchManagementWrapper);

+ 22 - 0
packages/app/src/components/Admin/FullTextSearchManagement.tsx

@@ -0,0 +1,22 @@
+import React, { FC } from 'react';
+
+import { useTranslation } from 'react-i18next';
+
+import ElasticsearchManagement from './ElasticsearchManagement/ElasticsearchManagement';
+
+type Props = {
+
+};
+
+const FullTextSearchManagement: FC<Props> = () => {
+  const { t } = useTranslation();
+
+  return (
+    <div data-testid="admin-full-text-search">
+      <h2> { t('full_text_search_management.elasticsearch_management') } </h2>
+      <ElasticsearchManagement />
+    </div>
+  );
+};
+
+export default FullTextSearchManagement;

+ 3 - 13
packages/app/src/components/Admin/ImportData/GrowiArchive/UploadForm.jsx

@@ -3,11 +3,8 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
-import AppContainer from '~/client/services/AppContainer';
 import { toastError } from '~/client/util/apiNotification';
-import { apiv3Post } from '~/client/util/apiv3-client';
-
-import { withUnstatedContainers } from '../../../UnstatedUtils';
+import { apiv3PostForm } from '~/client/util/apiv3-client';
 
 class UploadForm extends React.Component {
 
@@ -31,11 +28,10 @@ class UploadForm extends React.Component {
     e.preventDefault();
 
     const formData = new FormData();
-    formData.append('_csrf', this.props.appContainer.csrfToken);
     formData.append('file', this.inputRef.current.files[0]);
 
     try {
-      const { data } = await apiv3Post('/import/upload', formData);
+      const { data } = await apiv3PostForm('/import/upload', formData);
       // TODO: toastSuccess, toastError
       this.props.onUpload(data);
     }
@@ -96,15 +92,9 @@ class UploadForm extends React.Component {
 
 UploadForm.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   onUpload: PropTypes.func.isRequired,
   isTheSameVersion: PropTypes.bool,
   onVersionMismatch: PropTypes.func,
 };
 
-/**
- * Wrapper component for using unstated
- */
-const UploadFormWrapper = withUnstatedContainers(UploadForm, [AppContainer]);
-
-export default withTranslation()(UploadFormWrapper);
+export default withTranslation()(UploadForm);

+ 13 - 7
packages/app/src/components/Admin/ManageExternalAccount.jsx

@@ -1,14 +1,16 @@
 import React, { Fragment } from 'react';
+
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 
-import PaginationWrapper from '../PaginationWrapper';
+import AdminExternalAccountsContainer from '~/client/services/AdminExternalAccountsContainer';
+import AppContainer from '~/client/services/AppContainer';
+import { toastError } from '~/client/util/apiNotification';
 
+import PaginationWrapper from '../PaginationWrapper';
 import { withUnstatedContainers } from '../UnstatedUtils';
-import AppContainer from '~/client/services/AppContainer';
-import AdminExternalAccountsContainer from '~/client/services/AdminExternalAccountsContainer';
+
 import ExternalAccountTable from './Users/ExternalAccountTable';
-import { toastError } from '~/client/util/apiNotification';
 
 
 class ManageExternalAccount extends React.Component {
@@ -82,7 +84,11 @@ ManageExternalAccount.propTypes = {
   adminExternalAccountsContainer: PropTypes.instanceOf(AdminExternalAccountsContainer).isRequired,
 };
 
-const ManageExternalAccountWrapper = withUnstatedContainers(ManageExternalAccount, [AppContainer, AdminExternalAccountsContainer]);
+const ManageExternalAccountWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <ManageExternalAccount t={t} {...props} />;
+};
 
+const ManageExternalAccountWrapper = withUnstatedContainers(ManageExternalAccountWrapperFC, [AppContainer, AdminExternalAccountsContainer]);
 
-export default withTranslation()(ManageExternalAccountWrapper);
+export default ManageExternalAccountWrapper;

+ 9 - 6
packages/app/src/components/ArchiveCreateModal.jsx

@@ -1,7 +1,7 @@
 import React, { useState, useCallback } from 'react';
 
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
@@ -14,7 +14,8 @@ import { withUnstatedContainers } from './UnstatedUtils';
 
 
 const ArchiveCreateModal = (props) => {
-  const { t, appContainer } = props;
+  const { t } = useTranslation();
+  const { appContainer } = props;
   const [isCommentDownload, setIsCommentDownload] = useState(false);
   const [isAttachmentFileDownload, setIsAttachmentFileDownload] = useState(false);
   const [isSubordinatedPageDownload, setIsSubordinatedPageDownload] = useState(false);
@@ -233,10 +234,7 @@ const ArchiveCreateModal = (props) => {
   );
 };
 
-const ArchiveCreateModalWrapper = withUnstatedContainers(ArchiveCreateModal, [AppContainer]);
-
 ArchiveCreateModal.propTypes = {
-  t: PropTypes.func.isRequired, //  i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   isOpen: PropTypes.bool.isRequired,
   onClose: PropTypes.func,
@@ -245,4 +243,9 @@ ArchiveCreateModal.propTypes = {
   errorMessage: PropTypes.string,
 };
 
-export default withTranslation()(ArchiveCreateModalWrapper);
+/**
+ * Wrapper component for using unstated
+ */
+const ArchiveCreateModalWrapper = withUnstatedContainers(ArchiveCreateModal, [AppContainer]);
+
+export default ArchiveCreateModalWrapper;

+ 9 - 6
packages/app/src/components/Common/CountBadge.tsx

@@ -1,16 +1,19 @@
 import React, { FC } from 'react';
 
 type CountProps = {
-  count: number
+  count?: number,
+  offset?: number,
 }
 
 const CountBadge: FC<CountProps> = (props:CountProps) => {
+  const { count, offset = 0 } = props;
+
+
   return (
-    <>
-      <span className="grw-count-badge px-2 badge badge-pill badge-light">
-        {props.count}
-      </span>
-    </>
+    <span className="grw-count-badge px-2 badge badge-pill badge-light">
+      { count == null && <span className="text-muted">―</span> }
+      { count != null && count + offset }
+    </span>
   );
 };
 

+ 4 - 6
packages/app/src/components/CreateTemplateModal.jsx

@@ -2,13 +2,13 @@ import React from 'react';
 
 import { pathUtils } from '@growi/core';
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 import { Modal, ModalHeader, ModalBody } from 'reactstrap';
 import urljoin from 'url-join';
 
-
 const CreateTemplateModal = (props) => {
-  const { t, path } = props;
+  const { t } = useTranslation();
+  const { path } = props;
 
   const parentPath = pathUtils.addTrailingSlash(path);
 
@@ -63,12 +63,10 @@ const CreateTemplateModal = (props) => {
   );
 };
 
-
 CreateTemplateModal.propTypes = {
-  t: PropTypes.func.isRequired, //  i18next
   path: PropTypes.string.isRequired,
   isOpen: PropTypes.bool.isRequired,
   onClose: PropTypes.func.isRequired,
 };
 
-export default withTranslation()(CreateTemplateModal);
+export default CreateTemplateModal;

+ 6 - 9
packages/app/src/components/Drawio.tsx

@@ -10,11 +10,9 @@ import { debounce } from 'throttle-debounce';
 import NotAvailableForGuest from './NotAvailableForGuest';
 
 
-declare let window: {
-  globalEmitter: EventEmitter,
-  GraphViewer: {
-    createViewerForElement: (Element) => void,
-  };
+declare const globalEmitter: EventEmitter;
+declare const GraphViewer: {
+  createViewerForElement: (Element) => void,
 };
 
 type Props = {
@@ -35,7 +33,7 @@ const Drawio = (props: Props): JSX.Element => {
 
   const editButtonClickHandler = useCallback(() => {
     const { beginLineNumber, endLineNumber } = rangeLineNumberOfMarkdown;
-    window.globalEmitter.emit('launchDrawioModal', beginLineNumber, endLineNumber);
+    globalEmitter.emit('launchDrawioModal', beginLineNumber, endLineNumber);
   }, [rangeLineNumberOfMarkdown]);
 
   const renderDrawio = useCallback(() => {
@@ -50,21 +48,20 @@ const Drawio = (props: Props): JSX.Element => {
 
       if (div != null) {
         div.innerHTML = '';
-        window.GraphViewer.createViewerForElement(div);
+        GraphViewer.createViewerForElement(div);
       }
     }
   }, []);
 
   const renderDrawioWithDebounce = useMemo(() => debounce(200, renderDrawio), [renderDrawio]);
 
-  const { GraphViewer } = window;
   useEffect(() => {
     if (GraphViewer == null) {
       return;
     }
 
     renderDrawioWithDebounce();
-  }, [GraphViewer, renderDrawioWithDebounce]);
+  }, [renderDrawioWithDebounce]);
 
   return (
     <div className="editable-with-drawio position-relative">

+ 1 - 1
packages/app/src/components/FormattedDistanceDate.jsx

@@ -1,7 +1,7 @@
 import React from 'react';
-import PropTypes from 'prop-types';
 
 import { format, formatDistanceStrict, differenceInSeconds } from 'date-fns';
+import PropTypes from 'prop-types';
 import { UncontrolledTooltip } from 'reactstrap';
 
 const FormattedDistanceDate = (props) => {

+ 9 - 6
packages/app/src/components/InAppNotification/InAppNotificationDropdown.tsx

@@ -1,19 +1,22 @@
 import React, {
   useState, useEffect, FC, useCallback,
 } from 'react';
+
+import { useTranslation } from 'react-i18next';
 import {
   Dropdown, DropdownToggle, DropdownMenu, DropdownItem,
 } from 'reactstrap';
-import { useTranslation } from 'react-i18next';
-import loggerFactory from '~/utils/logger';
 
+import SocketIoContainer from '~/client/services/SocketIoContainer';
+import { toastError } from '~/client/util/apiNotification';
 import { apiv3Post } from '~/client/util/apiv3-client';
+import { useSWRxInAppNotifications, useSWRxInAppNotificationStatus } from '~/stores/in-app-notification';
+import loggerFactory from '~/utils/logger';
+
 import { withUnstatedContainers } from '../UnstatedUtils';
+
 import InAppNotificationList from './InAppNotificationList';
-import SocketIoContainer from '~/client/services/SocketIoContainer';
-import { useSWRxInAppNotifications, useSWRxInAppNotificationStatus } from '~/stores/in-app-notification';
 
-import { toastError } from '~/client/util/apiNotification';
 
 const logger = loggerFactory('growi:InAppNotificationDropdown');
 
@@ -74,7 +77,7 @@ const InAppNotificationDropdown: FC<Props> = (props: Props) => {
   }
 
   return (
-    <Dropdown className="notification-wrapper" isOpen={isOpen} toggle={toggleDropdownHandler}>
+    <Dropdown className="notification-wrapper grw-notification-dropdown" isOpen={isOpen} toggle={toggleDropdownHandler}>
       <DropdownToggle tag="a" className="px-3 nav-link border-0 bg-transparent waves-effect waves-light">
         <i className="icon-bell" /> {badge}
       </DropdownToggle>

+ 5 - 4
packages/app/src/components/InAppNotification/PageNotification/PageModelNotification.tsx

@@ -1,14 +1,15 @@
 import React, {
   forwardRef, ForwardRefRenderFunction, useImperativeHandle,
 } from 'react';
+
 import { PagePathLabel } from '@growi/ui';
 
 import { IInAppNotificationOpenable } from '~/client/interfaces/in-app-notification-openable';
-import { IInAppNotification } from '~/interfaces/in-app-notification';
 import { HasObjectId } from '~/interfaces/has-object-id';
+import { IInAppNotification } from '~/interfaces/in-app-notification';
 
-import FormattedDistanceDate from '../../FormattedDistanceDate';
 import { parseSnapshot } from '../../../models/serializers/in-app-notification-snapshot/page';
+import FormattedDistanceDate from '../../FormattedDistanceDate';
 
 interface Props {
   notification: IInAppNotification & HasObjectId
@@ -39,8 +40,8 @@ const PageModelNotification: ForwardRefRenderFunction<IInAppNotificationOpenable
   }));
 
   return (
-    <div className="p-2">
-      <div>
+    <div className="p-2 overflow-hidden">
+      <div className="text-truncate">
         <b>{actionUsers}</b> {actionMsg} <PagePathLabel path={snapshot.path} />
       </div>
       <i className={`${actionIcon} mr-2`} />

+ 13 - 5
packages/app/src/components/InstallerForm.jsx

@@ -1,10 +1,11 @@
 import React from 'react';
-import PropTypes from 'prop-types';
 
 import i18next from 'i18next';
-import { withTranslation } from 'react-i18next';
+import PropTypes from 'prop-types';
+import { useTranslation } from 'react-i18next';
 
 import { localeMetadatas } from '~/client/util/i18n';
+import { useCsrfToken } from '~/stores/context';
 
 class InstallerForm extends React.Component {
 
@@ -175,7 +176,7 @@ class InstallerForm extends React.Component {
               />
             </div>
 
-            <input type="hidden" name="_csrf" value={this.props.csrf} />
+            <input type="hidden" name="_csrf" value={this.props.csrfToken} />
 
             <div className="input-group mt-4 mb-3 d-flex justify-content-center">
               <button
@@ -211,7 +212,14 @@ InstallerForm.propTypes = {
   userName: PropTypes.string,
   name: PropTypes.string,
   email: PropTypes.string,
-  csrf: PropTypes.string,
+  csrfToken: PropTypes.string,
+};
+
+const InstallerFormWrapperFC = (props) => {
+  const { t } = useTranslation();
+  const { data: csrfToken } = useCsrfToken();
+
+  return <InstallerForm t={t} csrfToken={csrfToken} {...props} />;
 };
 
-export default withTranslation()(InstallerForm);
+export default InstallerFormWrapperFC;

+ 24 - 13
packages/app/src/components/LoginForm.jsx

@@ -1,10 +1,12 @@
 import React from 'react';
+
 import PropTypes from 'prop-types';
 import ReactCardFlip from 'react-card-flip';
-
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 
 import AppContainer from '~/client/services/AppContainer';
+import { useCsrfToken } from '~/stores/context';
+
 import { withUnstatedContainers } from './UnstatedUtils';
 
 class LoginForm extends React.Component {
@@ -35,12 +37,12 @@ class LoginForm extends React.Component {
 
   handleLoginWithExternalAuth(e) {
     const auth = e.currentTarget.id;
-    const { csrf } = this.props.appContainer;
-    window.location.href = `/passport/${auth}?_csrf=${csrf}`;
+    const { csrfToken } = this.props;
+    window.location.href = `/passport/${auth}?_csrf=${csrfToken}`;
   }
 
   renderLocalOrLdapLoginForm() {
-    const { t, appContainer, isLdapStrategySetup } = this.props;
+    const { t, csrfToken, isLdapStrategySetup } = this.props;
 
     return (
       <form role="form" action="/login" method="post">
@@ -70,7 +72,7 @@ class LoginForm extends React.Component {
         </div>
 
         <div className="input-group my-4">
-          <input type="hidden" name="_csrf" value={appContainer.csrfToken} />
+          <input type="hidden" name="_csrf" value={csrfToken} />
           <button type="submit" id="login" className="btn btn-fill rounded-0 login mx-auto" data-testid="btnSubmitForLogin">
             <div className="eff"></div>
             <span className="btn-label">
@@ -148,6 +150,7 @@ class LoginForm extends React.Component {
     const {
       t,
       appContainer,
+      csrfToken,
       isEmailAuthenticationEnabled,
       username,
       name,
@@ -251,7 +254,7 @@ class LoginForm extends React.Component {
           )}
 
           <div className="input-group justify-content-center my-4">
-            <input type="hidden" name="_csrf" value={appContainer.csrfToken} />
+            <input type="hidden" name="_csrf" value={csrfToken} />
             <button type="submit" className="btn btn-fill rounded-0" id="register" disabled={(!isMailerSetup && isEmailAuthenticationEnabled)}>
               <div className="eff"></div>
               <span className="btn-label">
@@ -327,16 +330,12 @@ class LoginForm extends React.Component {
 
 }
 
-/**
- * Wrapper component for using unstated
- */
-const LoginFormWrapper = withUnstatedContainers(LoginForm, [AppContainer]);
-
 LoginForm.propTypes = {
   // i18next
   t: PropTypes.func.isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 
+  csrfToken: PropTypes.string,
   isRegistering: PropTypes.bool,
   username: PropTypes.string,
   name: PropTypes.string,
@@ -351,4 +350,16 @@ LoginForm.propTypes = {
   objOfIsExternalAuthEnableds: PropTypes.object,
 };
 
-export default withTranslation()(LoginFormWrapper);
+const LoginFormWrapperFC = (props) => {
+  const { t } = useTranslation();
+  const { data: csrfToken } = useCsrfToken();
+
+  return <LoginForm t={t} csrfToken={csrfToken} {...props} />;
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const LoginFormWrapper = withUnstatedContainers(LoginFormWrapperFC, [AppContainer]);
+
+export default LoginFormWrapper;

+ 12 - 4
packages/app/src/components/Me/ApiSettings.jsx

@@ -2,7 +2,7 @@
 import React from 'react';
 
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 
 import AppContainer from '~/client/services/AppContainer';
 import PersonalContainer from '~/client/services/PersonalContainer';
@@ -97,12 +97,20 @@ class ApiSettings extends React.Component {
 
 }
 
-const ApiSettingsWrapper = withUnstatedContainers(ApiSettings, [AppContainer, PersonalContainer]);
-
 ApiSettings.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   personalContainer: PropTypes.instanceOf(PersonalContainer).isRequired,
 };
 
-export default withTranslation()(ApiSettingsWrapper);
+const ApiSettingsWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <ApiSettings t={t} {...props} />;
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const ApiSettingsWrapper = withUnstatedContainers(ApiSettingsWrapperFC, [AppContainer, PersonalContainer]);
+
+export default ApiSettingsWrapper;

+ 11 - 4
packages/app/src/components/Me/AssociateModal.jsx

@@ -2,7 +2,7 @@
 import React from 'react';
 
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 import {
   Modal,
   ModalHeader,
@@ -130,8 +130,6 @@ class AssociateModal extends React.Component {
 
 }
 
-const AssociateModalWrapper = withUnstatedContainers(AssociateModal, [AppContainer, PersonalContainer]);
-
 AssociateModal.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
@@ -141,5 +139,14 @@ AssociateModal.propTypes = {
   onClose: PropTypes.func.isRequired,
 };
 
+const AssociateModalWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <AssociateModal t={t} {...props} />;
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const AssociateModalWrapper = withUnstatedContainers(AssociateModalWrapperFC, [AppContainer, PersonalContainer]);
 
-export default withTranslation()(AssociateModalWrapper);
+export default AssociateModalWrapper;

+ 12 - 4
packages/app/src/components/Me/BasicInfoSettings.jsx

@@ -2,7 +2,7 @@
 import React, { Fragment } from 'react';
 
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 
 import PersonalContainer from '~/client/services/PersonalContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
@@ -163,11 +163,19 @@ class BasicInfoSettings extends React.Component {
 
 }
 
-const BasicInfoSettingsWrapper = withUnstatedContainers(BasicInfoSettings, [PersonalContainer]);
-
 BasicInfoSettings.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   personalContainer: PropTypes.instanceOf(PersonalContainer).isRequired,
 };
 
-export default withTranslation()(BasicInfoSettingsWrapper);
+const BasicInfoSettingsWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <BasicInfoSettings t={t} {...props} />;
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const BasicInfoSettingsWrapper = withUnstatedContainers(BasicInfoSettingsWrapperFC, [PersonalContainer]);
+
+export default BasicInfoSettingsWrapper;

+ 17 - 7
packages/app/src/components/Me/DisassociateModal.jsx

@@ -1,19 +1,21 @@
 
 import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
 
+import PropTypes from 'prop-types';
+import { useTranslation } from 'react-i18next';
 import {
   Modal,
   ModalHeader,
   ModalBody,
   ModalFooter,
 } from 'reactstrap';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
-import { withUnstatedContainers } from '../UnstatedUtils';
 
 import AppContainer from '~/client/services/AppContainer';
 import PersonalContainer from '~/client/services/PersonalContainer';
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
+
+import { withUnstatedContainers } from '../UnstatedUtils';
+
 
 class DisassociateModal extends React.Component {
 
@@ -71,8 +73,6 @@ class DisassociateModal extends React.Component {
 
 }
 
-const DisassociateModalWrapper = withUnstatedContainers(DisassociateModal, [AppContainer, PersonalContainer]);
-
 DisassociateModal.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
@@ -84,5 +84,15 @@ DisassociateModal.propTypes = {
 
 };
 
+const DisassociateModalWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <DisassociateModal t={t} {...props} />;
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const DisassociateModalWrapper = withUnstatedContainers(DisassociateModalWrapperFC, [AppContainer, PersonalContainer]);
+
 
-export default withTranslation()(DisassociateModalWrapper);
+export default DisassociateModalWrapper;

+ 12 - 4
packages/app/src/components/Me/ExternalAccountLinkedMe.jsx

@@ -2,7 +2,7 @@
 import React, { Fragment } from 'react';
 
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 
 
 import AppContainer from '~/client/services/AppContainer';
@@ -125,12 +125,20 @@ class ExternalAccountLinkedMe extends React.Component {
 
 }
 
-const ExternalAccountLinkedMeWrapper = withUnstatedContainers(ExternalAccountLinkedMe, [AppContainer, PersonalContainer]);
-
 ExternalAccountLinkedMe.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   personalContainer: PropTypes.instanceOf(PersonalContainer).isRequired,
 };
 
-export default withTranslation()(ExternalAccountLinkedMeWrapper);
+const ExternalAccountLinkedMeWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <ExternalAccountLinkedMe t={t} {...props} />;
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const ExternalAccountLinkedMeWrapper = withUnstatedContainers(ExternalAccountLinkedMeWrapperFC, [AppContainer, PersonalContainer]);
+
+export default ExternalAccountLinkedMeWrapper;

+ 5 - 7
packages/app/src/components/Me/ExternalAccountRow.jsx

@@ -1,12 +1,13 @@
 
 import React from 'react';
-import PropTypes from 'prop-types';
 
-import { withTranslation } from 'react-i18next';
 import dateFnsFormat from 'date-fns/format';
+import PropTypes from 'prop-types';
+import { useTranslation } from 'react-i18next';
 
 const ExternalAccountRow = (props) => {
-  const { t, account } = props;
+  const { t } = useTranslation();
+  const { account } = props;
 
   return (
     <tr>
@@ -29,12 +30,9 @@ const ExternalAccountRow = (props) => {
   );
 };
 
-
 ExternalAccountRow.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-
   account: PropTypes.object.isRequired,
   openDisassociateModal: PropTypes.func.isRequired,
 };
 
-export default withTranslation()(ExternalAccountRow);
+export default ExternalAccountRow;

+ 7 - 5
packages/app/src/components/Me/ImageCropModal.jsx

@@ -2,7 +2,7 @@ import React from 'react';
 
 import canvasToBlob from 'async-canvas-to-blob';
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 import ReactCrop from 'react-image-crop';
 import {
   Modal,
@@ -110,9 +110,6 @@ class ImageCropModal extends React.Component {
 
 }
 
-/**
- * Wrapper component for using unstated
- */
 ImageCropModal.propTypes = {
   show: PropTypes.bool.isRequired,
   src: PropTypes.string,
@@ -120,4 +117,9 @@ ImageCropModal.propTypes = {
   onCropCompleted: PropTypes.func.isRequired,
 };
 
-export default withTranslation()(ImageCropModal);
+const ImageCropModalWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <ImageCropModal t={t} {...props} />;
+};
+
+export default ImageCropModalWrapperFC;

+ 12 - 5
packages/app/src/components/Me/PasswordSettings.jsx

@@ -2,7 +2,7 @@
 import React from 'react';
 
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 
 import PersonalContainer from '~/client/services/PersonalContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
@@ -153,12 +153,19 @@ class PasswordSettings extends React.Component {
 
 }
 
-
-const PasswordSettingsWrapper = withUnstatedContainers(PasswordSettings, [PersonalContainer]);
-
 PasswordSettings.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   personalContainer: PropTypes.instanceOf(PersonalContainer).isRequired,
 };
 
-export default withTranslation()(PasswordSettingsWrapper);
+const PasswordSettingsWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <PasswordSettings t={t} {...props} />;
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const PasswordSettingsWrapper = withUnstatedContainers(PasswordSettingsWrapperFC, [PersonalContainer]);
+
+export default PasswordSettingsWrapper;

+ 4 - 8
packages/app/src/components/Me/PersonalSettings.jsx

@@ -2,7 +2,7 @@
 import React, { useMemo } from 'react';
 
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 
 import CustomNavAndContents from '../CustomNavigation/CustomNavAndContents';
 
@@ -13,9 +13,9 @@ import InAppNotificationSettings from './InAppNotificationSettings';
 import PasswordSettings from './PasswordSettings';
 import UserSettings from './UserSettings';
 
-const PersonalSettings = (props) => {
+const PersonalSettings = () => {
 
-  const { t } = props;
+  const { t } = useTranslation();
 
   const navTabMapping = useMemo(() => {
     return {
@@ -67,8 +67,4 @@ const PersonalSettings = (props) => {
 
 };
 
-PersonalSettings.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-};
-
-export default withTranslation()(PersonalSettings);
+export default PersonalSettings;

+ 5 - 16
packages/app/src/components/Me/ProfileImageSettings.tsx

@@ -3,29 +3,20 @@ import React, { useCallback, useState } from 'react';
 
 import { useTranslation } from 'react-i18next';
 
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
-import { apiPost } from '~/client/util/apiv1-client';
+import { apiPost, apiPostForm } from '~/client/util/apiv1-client';
 import { apiv3Put } from '~/client/util/apiv3-client';
 import { useCurrentUser } from '~/stores/context';
 import { generateGravatarSrc, GRAVATAR_DEFAULT } from '~/utils/gravatar';
 
-import { withUnstatedContainers } from '../UnstatedUtils';
-
 import ImageCropModal from './ImageCropModal';
 
 const DEFAULT_IMAGE = '/images/icons/user.svg';
 
 
-type Props = {
-  appContainer: AppContainer,
-}
-
-const ProfileImageSettings = (props: Props): JSX.Element => {
+const ProfileImageSettings = (): JSX.Element => {
   const { t } = useTranslation();
 
-  const { appContainer } = props;
-
   const { data: currentUser } = useCurrentUser();
 
   const [isGravatarEnabled, setGravatarEnabled] = useState(currentUser?.isGravatarEnabled);
@@ -55,9 +46,7 @@ const ProfileImageSettings = (props: Props): JSX.Element => {
     try {
       const formData = new FormData();
       formData.append('file', croppedImage);
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      formData.append('_csrf', appContainer.csrfToken!);
-      const response = await apiPost('/attachments.uploadProfileImage', formData);
+      const response = await apiPostForm('/attachments.uploadProfileImage', formData);
 
       toastSuccess(t('toaster.update_successed', { target: t('Current Image') }));
 
@@ -70,7 +59,7 @@ const ProfileImageSettings = (props: Props): JSX.Element => {
     catch (err) {
       toastError(err);
     }
-  }, [appContainer.csrfToken, t]);
+  }, [t]);
 
   const deleteImageHandler = useCallback(async() => {
     try {
@@ -185,4 +174,4 @@ const ProfileImageSettings = (props: Props): JSX.Element => {
 
 };
 
-export default withUnstatedContainers(ProfileImageSettings, [AppContainer]);
+export default ProfileImageSettings;

+ 0 - 36
packages/app/src/components/Me/UserSettings.jsx

@@ -1,36 +0,0 @@
-
-import React from 'react';
-
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import BasicInfoSettings from './BasicInfoSettings';
-import ProfileImageSettings from './ProfileImageSettings';
-
-class UserSettings extends React.Component {
-
-  render() {
-    const { t } = this.props;
-
-    return (
-      <div data-testid="grw-user-settings">
-        <div className="mb-5">
-          <h2 className="border-bottom my-4">{t('Basic Info')}</h2>
-          <BasicInfoSettings />
-        </div>
-        <div className="mb-5">
-          <h2 className="border-bottom my-4">{t('Set Profile Image')}</h2>
-          <ProfileImageSettings />
-        </div>
-      </div>
-    );
-  }
-
-}
-
-
-UserSettings.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-};
-
-export default withTranslation()(UserSettings);

+ 29 - 0
packages/app/src/components/Me/UserSettings.tsx

@@ -0,0 +1,29 @@
+import React, { FC } from 'react';
+
+import { useTranslation } from 'react-i18next';
+
+import BasicInfoSettings from './BasicInfoSettings';
+import ProfileImageSettings from './ProfileImageSettings';
+
+type Props = {
+
+};
+
+const UserSettings: FC<Props> = () => {
+  const { t } = useTranslation();
+
+  return (
+    <div data-testid="grw-user-settings">
+      <div className="mb-5">
+        <h2 className="border-bottom my-4">{t('Basic Info')}</h2>
+        <BasicInfoSettings />
+      </div>
+      <div className="mb-5">
+        <h2 className="border-bottom my-4">{t('Set Profile Image')}</h2>
+        <ProfileImageSettings />
+      </div>
+    </div>
+  );
+};
+
+export default UserSettings;

+ 15 - 12
packages/app/src/components/MyDraftList/Draft.jsx

@@ -1,18 +1,17 @@
 import React from 'react';
-import PropTypes from 'prop-types';
 
-import { withTranslation } from 'react-i18next';
+import PropTypes from 'prop-types';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
-
+import { useTranslation } from 'react-i18next';
 import {
   Collapse,
   UncontrolledTooltip,
 } from 'reactstrap';
 
-import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 
 import RevisionBody from '../Page/RevisionBody';
+import { withUnstatedContainers } from '../UnstatedUtils';
 
 class Draft extends React.Component {
 
@@ -60,7 +59,7 @@ class Draft extends React.Component {
     };
 
     const growiRenderer = this.growiRenderer;
-    const interceptorManager = this.props.appContainer.interceptorManager;
+    const { interceptorManager } = window;
     await interceptorManager.process('prePreProcess', context)
       .then(() => {
         context.markdown = growiRenderer.preProcess(context.markdown, context);
@@ -192,12 +191,6 @@ class Draft extends React.Component {
 
 }
 
-/**
- * Wrapper component for using unstated
- */
-const DraftWrapper = withUnstatedContainers(Draft, [AppContainer]);
-
-
 Draft.propTypes = {
   t: PropTypes.func.isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
@@ -209,4 +202,14 @@ Draft.propTypes = {
   clearDraft: PropTypes.func.isRequired,
 };
 
-export default withTranslation()(DraftWrapper);
+const DraftWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <Draft t={t} {...props} />;
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const DraftWrapper = withUnstatedContainers(DraftWrapperFC, [AppContainer]);
+
+export default DraftWrapper;

+ 12 - 8
packages/app/src/components/MyDraftList/MyDraftList.jsx

@@ -1,7 +1,7 @@
 import React from 'react';
 
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 
 import EditorContainer from '~/client/services/EditorContainer';
 import PageContainer from '~/client/services/PageContainer';
@@ -171,12 +171,6 @@ class MyDraftList extends React.Component {
 
 }
 
-/**
- * Wrapper component for using unstated
- */
-const MyDraftListWrapper = withUnstatedContainers(MyDraftList, [PageContainer, EditorContainer]);
-
-
 MyDraftList.propTypes = {
   t: PropTypes.func.isRequired, // react-i18next
 
@@ -184,4 +178,14 @@ MyDraftList.propTypes = {
   editorContainer: PropTypes.instanceOf(EditorContainer).isRequired,
 };
 
-export default withTranslation()(MyDraftListWrapper);
+const MyDraftListWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <MyDraftList t={t} {...props} />;
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const MyDraftListWrapper = withUnstatedContainers(MyDraftListWrapperFC, [PageContainer, EditorContainer]);
+
+export default MyDraftListWrapper;

+ 1 - 4
packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -282,8 +282,7 @@ const GrowiContextualSubNavigation = (props) => {
             />
           )}
         </div>
-
-        {currentUser != null && (
+        {path != null && currentUser != null && (
           <CreateTemplateModal
             path={path}
             isOpen={isPageTemplateModalShown}
@@ -300,7 +299,6 @@ const GrowiContextualSubNavigation = (props) => {
     path, templateMenuItemClickHandler, isPageTemplateModalShown,
   ]);
 
-
   if (path == null) {
     return <></>;
   }
@@ -315,7 +313,6 @@ const GrowiContextualSubNavigation = (props) => {
     updatedAt: updatedAt ?? undefined,
   };
 
-
   return (
     <GrowiSubNavigation
       page={currentPage}

+ 7 - 10
packages/app/src/components/Page/CopyDropdown.jsx

@@ -1,18 +1,16 @@
 import React, {
   useState, useMemo, useCallback,
 } from 'react';
-import PropTypes from 'prop-types';
-
-import { withTranslation } from 'react-i18next';
 
+import { pagePathUtils } from '@growi/core';
+import PropTypes from 'prop-types';
+import { CopyToClipboard } from 'react-copy-to-clipboard';
+import { useTranslation } from 'react-i18next';
 import {
   Dropdown, DropdownToggle, DropdownMenu, DropdownItem,
   Tooltip,
 } from 'reactstrap';
 
-import { CopyToClipboard } from 'react-copy-to-clipboard';
-
-import { pagePathUtils } from '@growi/core';
 
 const { encodeSpaces } = pagePathUtils;
 
@@ -102,8 +100,9 @@ const CopyDropdown = (props) => {
   /*
    * render
    */
+  const { t } = useTranslation();
   const {
-    t, dropdownToggleId, pageId, dropdownToggleClassName, children, isShareLinkMode,
+    dropdownToggleId, pageId, dropdownToggleClassName, children, isShareLinkMode,
   } = props;
 
   const customSwitchForParamsId = `customSwitchForParams_${dropdownToggleId}`;
@@ -199,8 +198,6 @@ const CopyDropdown = (props) => {
 };
 
 CopyDropdown.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-
   children: PropTypes.node.isRequired,
   dropdownToggleId: PropTypes.string.isRequired,
   pagePath: PropTypes.string.isRequired,
@@ -210,4 +207,4 @@ CopyDropdown.propTypes = {
   isShareLinkMode: PropTypes.bool,
 };
 
-export default withTranslation()(CopyDropdown);
+export default CopyDropdown;

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

@@ -76,7 +76,7 @@ const DisplaySwitcher = (): JSX.Element => {
                           <PageListIcon />
                         </div>
                         {t('page_list')}
-                        {currentPage?.descendantCount != null && <CountBadge count={currentPage.descendantCount + 1} />}
+                        <CountBadge count={currentPage?.descendantCount} offset={1} />
                       </button>
                     ) }
                   </div>
@@ -91,7 +91,7 @@ const DisplaySwitcher = (): JSX.Element => {
                       >
                         <i className="icon-fw icon-bubbles grw-page-accessories-control-icon"></i>
                         <span>Comments</span>
-                        {currentPage?.commentCount != null && <CountBadge count={currentPage.commentCount} />}
+                        <CountBadge count={currentPage?.commentCount} />
                       </button>
                     </div>
                   ) }

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

@@ -58,7 +58,12 @@ export default class RevisionBody extends React.PureComponent {
     const additionalClassName = this.props.additionalClassName || '';
     return (
       <div
-        ref={this.props.inputRef}
+        ref={(elem) => {
+          this.element = elem;
+          if (this.props.inputRef != null) {
+            this.props.inputRef.current = elem;
+          }
+        }}
         id="wiki"
         className={`wiki ${additionalClassName}`}
         // eslint-disable-next-line react/no-danger

+ 2 - 2
packages/app/src/components/Page/RevisionRenderer.jsx

@@ -58,7 +58,7 @@ class LegacyRevisionRenderer extends React.PureComponent {
     const HeaderLinkArray = Array.from(HeaderLink);
     addSmoothScrollEvent(HeaderLinkArray, blinkElem);
 
-    const { interceptorManager } = this.props.appContainer;
+    const { interceptorManager } = window;
 
     interceptorManager.process('postRenderHtml', this.currentRenderingContext);
   }
@@ -134,7 +134,7 @@ class LegacyRevisionRenderer extends React.PureComponent {
       highlightKeywords,
     } = this.props;
 
-    const { interceptorManager } = appContainer;
+    const { interceptorManager } = window;
     const context = this.currentRenderingContext;
 
     await interceptorManager.process('preRender', context);

+ 4 - 11
packages/app/src/components/Page/ShareLinkAlert.jsx

@@ -1,11 +1,9 @@
 import React from 'react';
-import PropTypes from 'prop-types';
 
-import { withTranslation } from 'react-i18next';
-
-const ShareLinkAlert = (props) => {
-  const { t } = props;
+import { useTranslation } from 'react-i18next';
 
+const ShareLinkAlert = () => {
+  const { t } = useTranslation();
 
   const shareContent = document.getElementById('is-shared-page');
   const expiredAt = shareContent.getAttribute('data-share-link-expired-at');
@@ -51,9 +49,4 @@ const ShareLinkAlert = (props) => {
   );
 };
 
-
-ShareLinkAlert.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-};
-
-export default withTranslation()(ShareLinkAlert);
+export default ShareLinkAlert;

+ 8 - 9
packages/app/src/components/Page/TrashPageAlert.jsx

@@ -3,7 +3,7 @@ import React, { useState } from 'react';
 
 import { UserPicture } from '@growi/ui';
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 
 import PageContainer from '~/client/services/PageContainer';
 import { useCurrentUpdatedAt, useShareLinkId } from '~/stores/context';
@@ -22,7 +22,8 @@ const onDeletedHandler = (pathOrPathsToDelete, isRecursively, isCompletely) => {
 };
 
 const TrashPageAlert = (props) => {
-  const { t, pageContainer } = props;
+  const { t } = useTranslation();
+  const { pageContainer } = props;
   const {
     pageId, revisionId, path, isDeleted, lastUpdateUsername, deletedUserName, deletedAt,
   } = pageContainer.state;
@@ -141,15 +142,13 @@ const TrashPageAlert = (props) => {
   );
 };
 
+TrashPageAlert.propTypes = {
+  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
+};
+
 /**
  * Wrapper component for using unstated
  */
 const TrashPageAlertWrapper = withUnstatedContainers(TrashPageAlert, [PageContainer]);
 
-
-TrashPageAlert.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
-};
-
-export default withTranslation()(TrashPageAlertWrapper);
+export default TrashPageAlertWrapper;

+ 15 - 16
packages/app/src/components/PageAccessoriesModalControl.jsx

@@ -1,23 +1,23 @@
 import React, { Fragment, useMemo } from 'react';
-import PropTypes from 'prop-types';
-
-import { withTranslation } from 'react-i18next';
 
+import PropTypes from 'prop-types';
+import { useTranslation } from 'react-i18next';
 import { UncontrolledTooltip } from 'reactstrap';
 
-import PageListIcon from './Icons/PageListIcon';
-import TimeLineIcon from './Icons/TimeLineIcon';
-import HistoryIcon from './Icons/HistoryIcon';
+import { useCurrentPageId } from '~/stores/context';
+
 import AttachmentIcon from './Icons/AttachmentIcon';
+import HistoryIcon from './Icons/HistoryIcon';
+import PageListIcon from './Icons/PageListIcon';
 import ShareLinkIcon from './Icons/ShareLinkIcon';
-
+import TimeLineIcon from './Icons/TimeLineIcon';
 import { withUnstatedContainers } from './UnstatedUtils';
 
-import { useCurrentPageId } from '~/stores/context';
 
 const PageAccessoriesModalControl = (props) => {
+  const { t } = useTranslation();
   const {
-    t, pageAccessoriesContainer, isGuestUser, isSharedUser,
+    pageAccessoriesContainer, isGuestUser, isSharedUser,
   } = props;
   const isLinkSharingDisabled = pageAccessoriesContainer.appContainer.config.disableLinkSharing;
 
@@ -92,18 +92,17 @@ const PageAccessoriesModalControl = (props) => {
     </div>
   );
 };
-/**
- * Wrapper component for using unstated
- */
-const PageAccessoriesModalControlWrapper = withUnstatedContainers(PageAccessoriesModalControl, []);
 
 PageAccessoriesModalControl.propTypes = {
-  t: PropTypes.func.isRequired, //  i18next
-
   pageAccessoriesContainer: PropTypes.any,
 
   isGuestUser: PropTypes.bool.isRequired,
   isSharedUser: PropTypes.bool.isRequired,
 };
 
-export default withTranslation()(PageAccessoriesModalControlWrapper);
+/**
+ * Wrapper component for using unstated
+ */
+const PageAccessoriesModalControlWrapper = withUnstatedContainers(PageAccessoriesModalControl, []);
+
+export default PageAccessoriesModalControlWrapper;

+ 2 - 2
packages/app/src/components/PageComment/Comment.jsx

@@ -69,7 +69,7 @@ class Comment extends React.PureComponent {
       return;
     }
 
-    const { interceptorManager } = this.props.appContainer;
+    const { interceptorManager } = window;
 
     interceptorManager.process('postRenderCommentHtml', this.currentRenderingContext);
   }
@@ -133,7 +133,7 @@ class Comment extends React.PureComponent {
   async renderHtml() {
 
     const { growiRenderer, appContainer } = this.props;
-    const { interceptorManager } = appContainer;
+    const { interceptorManager } = window;
     const context = this.currentRenderingContext;
 
     await interceptorManager.process('preRenderComment', context);

+ 5 - 6
packages/app/src/components/PageComment/CommentEditor.jsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useCallback } from 'react';
 
 import { UserPicture } from '@growi/ui';
 import PropTypes from 'prop-types';
@@ -224,7 +224,6 @@ class CommentEditor extends React.Component {
   getCommentHtml() {
     return (
       <CommentPreview
-        inputRef={(el) => { this.previewElement = el }}
         html={this.state.html}
       />
     );
@@ -236,7 +235,7 @@ class CommentEditor extends React.Component {
     };
 
     const { growiRenderer } = this.props;
-    const interceptorManager = this.props.appContainer.interceptorManager;
+    const { interceptorManager } = window;
     interceptorManager.process('preRenderCommnetPreview', context)
       .then(() => { return interceptorManager.process('prePreProcess', context) })
       .then(() => {
@@ -281,7 +280,7 @@ class CommentEditor extends React.Component {
   }
 
   renderReady() {
-    const { appContainer, commentContainer, isMobile } = this.props;
+    const { isMobile } = this.props;
     const { activeTab } = this.state;
 
     const commentPreview = this.state.isMarkdown ? this.getCommentHtml() : null;
@@ -446,9 +445,9 @@ const CommentEditorWrapper = (props) => {
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: slackChannelsData } = useSWRxSlackChannels(currentPagePath);
 
-  const onSlackEnabledFlagChange = (isSlackEnabled) => {
+  const onSlackEnabledFlagChange = useCallback((isSlackEnabled) => {
     mutateIsSlackEnabled(isSlackEnabled, false);
-  };
+  }, [mutateIsSlackEnabled]);
 
   return (
     <CommentEditorHOCWrapper

+ 15 - 20
packages/app/src/components/PageComment/CommentPreview.jsx

@@ -1,4 +1,5 @@
 import React from 'react';
+
 import PropTypes from 'prop-types';
 
 import RevisionBody from '../Page/RevisionBody';
@@ -6,29 +7,23 @@ import RevisionBody from '../Page/RevisionBody';
 /**
  * Wrapper component for Page/RevisionBody
  */
-export default class CommentPreview extends React.Component {
-
-  render() {
-    return (
-      <div
-        className="page-comment-preview-body"
-        ref={(elm) => {
-          this.previewElement = elm;
-          this.props.inputRef(elm);
-        }}
-      >
+const CommentPreview = (props) => {
 
-        <RevisionBody
-          {...this.props}
-          additionalClassName="comment"
-        />
-      </div>
-    );
-  }
+  return (
+    <div className="page-comment-preview-body">
+      <RevisionBody
+        html={props.html}
+        additionalClassName="comment"
+        isMathJaxEnabled
+        renderMathJaxInRealtime
+      />
+    </div>
+  );
 
-}
+};
 
 CommentPreview.propTypes = {
   html: PropTypes.string,
-  inputRef: PropTypes.func.isRequired, // for getting div element
 };
+
+export default CommentPreview;

+ 8 - 10
packages/app/src/components/PageCreateModal.jsx

@@ -5,7 +5,7 @@ import React, {
 import { pagePathUtils, pathUtils } from '@growi/core';
 import { format } from 'date-fns';
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 import { Modal, ModalHeader, ModalBody } from 'reactstrap';
 import { debounce } from 'throttle-debounce';
 
@@ -24,7 +24,8 @@ const {
 } = pagePathUtils;
 
 const PageCreateModal = (props) => {
-  const { t, appContainer } = props;
+  const { t } = useTranslation();
+  const { appContainer } = props;
 
   const { data: currentUser } = useCurrentUser();
 
@@ -310,16 +311,13 @@ const PageCreateModal = (props) => {
   );
 };
 
+PageCreateModal.propTypes = {
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+};
 
 /**
  * Wrapper component for using unstated
  */
-const ModalControlWrapper = withUnstatedContainers(PageCreateModal, [AppContainer]);
-
-
-PageCreateModal.propTypes = {
-  t: PropTypes.func.isRequired, //  i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-};
+const PageCreateModalWrapper = withUnstatedContainers(PageCreateModal, [AppContainer]);
 
-export default withTranslation()(ModalControlWrapper);
+export default PageCreateModalWrapper;

+ 9 - 12
packages/app/src/components/PageEditor.tsx

@@ -11,7 +11,7 @@ import { throttle, debounce } from 'throttle-debounce';
 import AppContainer from '~/client/services/AppContainer';
 import EditorContainer from '~/client/services/EditorContainer';
 import PageContainer from '~/client/services/PageContainer';
-import { apiGet, apiPost } from '~/client/util/apiv1-client';
+import { apiGet, apiPostForm } from '~/client/util/apiv1-client';
 import { getOptionsToSave } from '~/client/util/editor';
 import { useIsEditable, useIsIndentSizeForced, useCurrentPagePath } from '~/stores/context';
 import {
@@ -36,9 +36,8 @@ import { withUnstatedContainers } from './UnstatedUtils';
 const logger = loggerFactory('growi:PageEditor');
 
 
-declare let window: {
-  globalEmitter: EventEmitter,
-};
+declare const globalEmitter: EventEmitter;
+
 
 type EditorRef = {
   setValue: (markdown: string) => void,
@@ -165,8 +164,6 @@ const PageEditor = (props: Props): JSX.Element => {
 
       const formData = new FormData();
       const { pageId, path } = pageContainer.state;
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      formData.append('_csrf', appContainer.csrfToken!);
       formData.append('file', file);
       if (path != null) {
         formData.append('path', path);
@@ -175,7 +172,7 @@ const PageEditor = (props: Props): JSX.Element => {
         formData.append('page_id', pageId);
       }
 
-      res = await apiPost('/attachments.add', formData);
+      res = await apiPostForm('/attachments.add', formData);
       const attachment = res.attachment;
       const fileName = attachment.originalName;
 
@@ -201,7 +198,7 @@ const PageEditor = (props: Props): JSX.Element => {
     finally {
       editorRef.current.terminateUploadingState();
     }
-  }, [appContainer.csrfToken, editorMode, mutateGrant, pageContainer]);
+  }, [editorMode, mutateGrant, pageContainer]);
 
 
   const scrollPreviewByEditorLine = useCallback((line: number) => {
@@ -322,10 +319,10 @@ const PageEditor = (props: Props): JSX.Element => {
         scrollSyncHelper.scrollPreview(previewRef.current, line);
       }
     };
-    window.globalEmitter.on('setCaretLine', handler);
+    globalEmitter.on('setCaretLine', handler);
 
     return function cleanup() {
-      window.globalEmitter.removeListener('setCaretLine', handler);
+      globalEmitter.removeListener('setCaretLine', handler);
     };
   }, []);
 
@@ -343,10 +340,10 @@ const PageEditor = (props: Props): JSX.Element => {
         editorRef.current.setValue(markdown);
       }
     };
-    window.globalEmitter.on('updateEditorValue', handler);
+    globalEmitter.on('updateEditorValue', handler);
 
     return function cleanup() {
-      window.globalEmitter.removeListener('updateEditorValue', handler);
+      globalEmitter.removeListener('updateEditorValue', handler);
     };
   }, []);
 

+ 8 - 2
packages/app/src/components/PageEditor/Cheatsheet.jsx

@@ -1,8 +1,9 @@
 /* eslint-disable max-len */
 
 import React from 'react';
+
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 
 class Cheatsheet extends React.Component {
 
@@ -103,4 +104,9 @@ Cheatsheet.propTypes = {
   t: PropTypes.func.isRequired, // i18next
 };
 
-export default withTranslation()(Cheatsheet);
+const CheatsheetWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <Cheatsheet t={t} {...props} />;
+};
+
+export default CheatsheetWrapperFC;

+ 5 - 2
packages/app/src/components/PageEditor/Preview.tsx

@@ -4,12 +4,16 @@ import React, {
 
 
 import AppContainer from '~/client/services/AppContainer';
+import InterceptorManager from '~/services/interceptor-manager';
 import { useEditorSettings } from '~/stores/editor';
 
 import RevisionBody from '../Page/RevisionBody';
 import { withUnstatedContainers } from '../UnstatedUtils';
 
 
+declare const interceptorManager: InterceptorManager;
+
+
 type Props = {
   appContainer: AppContainer,
 
@@ -34,8 +38,7 @@ const Preview = (props: Props): JSX.Element => {
 
   const { data: editorSettings } = useEditorSettings();
 
-  const { interceptorManager } = appContainer;
-  const growiRenderer = props.appContainer.getRenderer('editor');
+  const growiRenderer = appContainer.getRenderer('editor');
 
   const context = useMemo(() => {
     return {

+ 8 - 2
packages/app/src/components/PageEditor/SimpleCheatsheet.jsx

@@ -1,6 +1,7 @@
 import React from 'react';
+
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 
 class SimpleCheatsheet extends React.Component {
 
@@ -50,4 +51,9 @@ SimpleCheatsheet.propTypes = {
   t: PropTypes.func.isRequired, // i18next
 };
 
-export default withTranslation()(SimpleCheatsheet);
+const SimpleCheatsheetWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <SimpleCheatsheet t={t} {...props} />;
+};
+
+export default SimpleCheatsheetWrapperFC;

+ 20 - 18
packages/app/src/components/PageEditorByHackmd.jsx

@@ -1,7 +1,7 @@
 import React, { useState, useEffect } from 'react';
 
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 
 
 import AppContainer from '~/client/services/AppContainer';
@@ -424,12 +424,29 @@ class PageEditorByHackmd extends React.Component {
 
 }
 
+PageEditorByHackmd.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
+  editorContainer: PropTypes.instanceOf(EditorContainer).isRequired,
+
+  // TODO: remove this when omitting unstated is completed
+  editorMode: PropTypes.string.isRequired,
+  isSlackEnabled: PropTypes.bool.isRequired,
+  slackChannels: PropTypes.string.isRequired,
+  grant: PropTypes.number.isRequired,
+  grantGroupId: PropTypes.string,
+  grantGroupName: PropTypes.string,
+};
+
 /**
  * Wrapper component for using unstated
  */
 const PageEditorByHackmdHOCWrapper = withUnstatedContainers(PageEditorByHackmd, [AppContainer, PageContainer, EditorContainer]);
 
 const PageEditorByHackmdWrapper = (props) => {
+  const { t } = useTranslation();
   const { data: editorMode } = useEditorMode();
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: slackChannelsData } = useSWRxSlackChannels(currentPagePath);
@@ -445,6 +462,7 @@ const PageEditorByHackmdWrapper = (props) => {
   return (
     <PageEditorByHackmdHOCWrapper
       {...props}
+      t={t}
       editorMode={editorMode}
       isSlackEnabled={isSlackEnabled}
       slackChannels={slackChannelsData.toString()}
@@ -455,20 +473,4 @@ const PageEditorByHackmdWrapper = (props) => {
   );
 };
 
-PageEditorByHackmd.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
-  editorContainer: PropTypes.instanceOf(EditorContainer).isRequired,
-
-  // TODO: remove this when omitting unstated is completed
-  editorMode: PropTypes.string.isRequired,
-  isSlackEnabled: PropTypes.bool.isRequired,
-  slackChannels: PropTypes.string.isRequired,
-  grant: PropTypes.number.isRequired,
-  grantGroupId: PropTypes.string,
-  grantGroupName: PropTypes.string,
-};
-
-export default withTranslation()(PageEditorByHackmdWrapper);
+export default PageEditorByHackmdWrapper;

+ 8 - 2
packages/app/src/components/PageHistory/PageRevisionTable.jsx

@@ -1,7 +1,8 @@
 import React from 'react';
+
 import PropTypes from 'prop-types';
+import { useTranslation } from 'react-i18next';
 
-import { withTranslation } from 'react-i18next';
 import PageHistroyContainer from '~/client/services/PageHistoryContainer';
 import RevisionComparerContainer from '~/client/services/RevisionComparerContainer';
 
@@ -162,4 +163,9 @@ PageRevisionTable.propTypes = {
   diffOpened: PropTypes.object,
 };
 
-export default withTranslation()(PageRevisionTable);
+const PageRevisionTableWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <PageRevisionTable t={t} {...props} />;
+};
+
+export default PageRevisionTableWrapperFC;

+ 10 - 3
packages/app/src/components/PageHistory/RevisionDiff.jsx

@@ -1,10 +1,12 @@
 /* eslint-disable react/no-danger */
 import React from 'react';
-import PropTypes from 'prop-types';
+
 
 import { createPatch } from 'diff';
 import { html } from 'diff2html';
-import { withTranslation } from 'react-i18next';
+import PropTypes from 'prop-types';
+import { useTranslation } from 'react-i18next';
+
 import UserDate from '../User/UserDate';
 
 class RevisionDiff extends React.Component {
@@ -77,4 +79,9 @@ RevisionDiff.propTypes = {
   revisionDiffOpened: PropTypes.bool.isRequired,
 };
 
-export default withTranslation()(RevisionDiff);
+const RevisionDiffWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <RevisionDiff t={t} {...props} />;
+};
+
+export default RevisionDiffWrapperFC;

+ 5 - 6
packages/app/src/components/PageManagement/ApiErrorMessage.jsx

@@ -1,11 +1,12 @@
 import React from 'react';
-import PropTypes from 'prop-types';
 
-import { withTranslation } from 'react-i18next';
+import PropTypes from 'prop-types';
+import { useTranslation } from 'react-i18next';
 
 const ApiErrorMessage = (props) => {
+  const { t } = useTranslation();
   const {
-    t, errorCode, errorMessage, targetPath,
+    errorCode, errorMessage, targetPath,
   } = props;
 
   function reload() {
@@ -71,11 +72,9 @@ const ApiErrorMessage = (props) => {
 };
 
 ApiErrorMessage.propTypes = {
-  t:            PropTypes.func.isRequired, //  i18next
-
   errorCode:    PropTypes.string,
   errorMessage: PropTypes.string,
   targetPath:   PropTypes.string,
 };
 
-export default withTranslation()(ApiErrorMessage);
+export default ApiErrorMessage;

+ 12 - 7
packages/app/src/components/PageStatusAlert.jsx

@@ -1,7 +1,7 @@
 import React from 'react';
 
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 
 import AppContainer from '~/client/services/AppContainer';
 import PageContainer from '~/client/services/PageContainer';
@@ -156,11 +156,6 @@ class PageStatusAlert extends React.Component {
 
 }
 
-/**
- * Wrapper component for using unstated
- */
-const PageStatusAlertWrapper = withUnstatedContainers(PageStatusAlert, [AppContainer, PageContainer]);
-
 PageStatusAlert.propTypes = {
   t: PropTypes.func.isRequired, // i18next
 
@@ -168,4 +163,14 @@ PageStatusAlert.propTypes = {
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
 };
 
-export default withTranslation()(PageStatusAlertWrapper);
+const PageStatusAlertWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <PageStatusAlert t={t} {...props} />;
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const PageStatusAlertWrapper = withUnstatedContainers(PageStatusAlertWrapperFC, [AppContainer, PageContainer]);
+
+export default PageStatusAlertWrapper;

+ 12 - 7
packages/app/src/components/PageTimeline.jsx

@@ -1,7 +1,7 @@
 import React from 'react';
 
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 
 import AppContainer from '~/client/services/AppContainer';
 import PageContainer from '~/client/services/PageContainer';
@@ -107,11 +107,6 @@ class PageTimeline extends React.Component {
 
 }
 
-/**
- * Wrapper component for using unstated
- */
-const PageTimelineWrapper = withUnstatedContainers(PageTimeline, [AppContainer, PageContainer]);
-
 PageTimeline.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
@@ -119,4 +114,14 @@ PageTimeline.propTypes = {
   pages: PropTypes.arrayOf(PropTypes.object),
 };
 
-export default withTranslation()(PageTimelineWrapper);
+const PageTimelineWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <PageTimeline t={t} {...props} />;
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const PageTimelineWrapper = withUnstatedContainers(PageTimelineWrapperFC, [AppContainer, PageContainer]);
+
+export default PageTimelineWrapper;

+ 3 - 11
packages/app/src/components/PasswordResetExecutionForm.jsx

@@ -1,20 +1,16 @@
 import React, { useState } from 'react';
 
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Put } from '~/client/util/apiv3-client';
 import loggerFactory from '~/utils/logger';
 
-import { withUnstatedContainers } from './UnstatedUtils';
-
 const logger = loggerFactory('growi:passwordReset');
 
 
 const PasswordResetExecutionForm = (props) => {
-  const { t, appContainer } = props;
+  const { t } = useTranslation();
 
   const [newPassword, setNewPassword] = useState('');
   const [newPasswordConfirm, setNewPasswordConfirm] = useState('');
@@ -90,11 +86,7 @@ const PasswordResetExecutionForm = (props) => {
   );
 };
 
-const PasswordResetExecutionFormWrapper = withUnstatedContainers(PasswordResetExecutionForm, [AppContainer]);
-
 PasswordResetExecutionForm.propTypes = {
-  t: PropTypes.func.isRequired, //  i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 };
 
-export default withTranslation()(PasswordResetExecutionFormWrapper);
+export default PasswordResetExecutionForm;

+ 3 - 14
packages/app/src/components/PasswordResetRequestForm.jsx

@@ -1,17 +1,13 @@
 import React, { useState } from 'react';
 
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Post } from '~/client/util/apiv3-client';
 
-import { withUnstatedContainers } from './UnstatedUtils';
-
 
 const PasswordResetRequestForm = (props) => {
-  const { t, appContainer } = props;
+  const { t } = useTranslation();
   const [email, setEmail] = useState('');
 
   const changeEmail = (inputValue) => {
@@ -56,14 +52,7 @@ const PasswordResetRequestForm = (props) => {
   );
 };
 
-/**
- * Wrapper component for using unstated
- */
-const PasswordResetRequestFormWrapper = withUnstatedContainers(PasswordResetRequestForm, [AppContainer]);
-
 PasswordResetRequestForm.propTypes = {
-  t: PropTypes.func.isRequired, //  i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 };
 
-export default withTranslation()(PasswordResetRequestFormWrapper);
+export default PasswordResetRequestForm;

+ 9 - 10
packages/app/src/components/RevisionComparer/RevisionComparer.jsx

@@ -3,7 +3,7 @@ import React, { useState } from 'react';
 import { pagePathUtils } from '@growi/core';
 import PropTypes from 'prop-types';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 import {
   Dropdown, DropdownToggle, DropdownMenu, DropdownItem,
 } from 'reactstrap';
@@ -31,7 +31,8 @@ const RevisionComparer = (props) => {
 
   const [dropdownOpen, setDropdownOpen] = useState(false);
 
-  const { t, revisionComparerContainer } = props;
+  const { t } = useTranslation();
+  const { revisionComparerContainer } = props;
 
   const { path, pageId } = revisionComparerContainer.pageContainer.state;
 
@@ -113,16 +114,14 @@ const RevisionComparer = (props) => {
   );
 };
 
-/**
- * Wrapper component for using unstated
- */
-const RevisionComparerWrapper = withUnstatedContainers(RevisionComparer, [RevisionComparerContainer]);
-
 RevisionComparer.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
   revisionComparerContainer: PropTypes.instanceOf(RevisionComparerContainer).isRequired,
-
   revisions: PropTypes.array,
 };
 
-export default withTranslation()(RevisionComparerWrapper);
+/**
+ * Wrapper component for using unstated
+ */
+const RevisionComparerWrapper = withUnstatedContainers(RevisionComparer, [RevisionComparerContainer]);
+
+export default RevisionComparerWrapper;

+ 4 - 2
packages/app/src/components/SavePageControls.jsx

@@ -1,7 +1,7 @@
 import React from 'react';
 
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 import {
   UncontrolledButtonDropdown, Button,
   DropdownToggle, DropdownMenu, DropdownItem,
@@ -137,6 +137,7 @@ class SavePageControls extends React.Component {
 const SavePageControlsHOCWrapper = withUnstatedContainers(SavePageControls, [AppContainer, PageContainer, EditorContainer]);
 
 const SavePageControlsWrapper = (props) => {
+  const { t } = useTranslation();
   const { data: isEditable } = useIsEditable();
   const { data: editorMode } = useEditorMode();
   const { data: grant, mutate: mutateGrant } = useSelectedGrant();
@@ -154,6 +155,7 @@ const SavePageControlsWrapper = (props) => {
 
   return (
     <SavePageControlsHOCWrapper
+      t={t}
       {...props}
       editorMode={editorMode}
       grant={grant}
@@ -185,4 +187,4 @@ SavePageControls.propTypes = {
   mutateGrantGroupName: PropTypes.func,
 };
 
-export default withTranslation()(SavePageControlsWrapper);
+export default SavePageControlsWrapper;

+ 12 - 7
packages/app/src/components/ShareLink/ShareLink.jsx

@@ -1,7 +1,7 @@
 import React from 'react';
 
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 
 
 import PageContainer from '~/client/services/PageContainer';
@@ -112,14 +112,19 @@ class ShareLink extends React.Component {
 
 }
 
-/**
- * Wrapper component for using unstated
- */
-const ShareLinkWrapper = withUnstatedContainers(ShareLink, [PageContainer]);
-
 ShareLink.propTypes = {
   t: PropTypes.func.isRequired, //  i18next
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
 };
 
-export default withTranslation()(ShareLinkWrapper);
+const ShareLinkWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <ShareLink t={t} {...props} />;
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const ShareLinkWrapper = withUnstatedContainers(ShareLinkWrapperFC, [PageContainer]);
+
+export default ShareLinkWrapper;

+ 12 - 7
packages/app/src/components/ShareLink/ShareLinkForm.jsx

@@ -3,7 +3,7 @@ import React from 'react';
 import { isInteger } from 'core-js/fn/number';
 import { format, parse } from 'date-fns';
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 
 import PageContainer from '~/client/services/PageContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
@@ -258,15 +258,20 @@ class ShareLinkForm extends React.Component {
 
 }
 
-/**
- * Wrapper component for using unstated
- */
-const ShareLinkFormWrapper = withUnstatedContainers(ShareLinkForm, [PageContainer]);
-
 ShareLinkForm.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
   onCloseForm: PropTypes.func,
 };
 
-export default withTranslation()(ShareLinkFormWrapper);
+const ShareLinkFormWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <ShareLinkForm t={t} {...props} />;
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const ShareLinkFormWrapper = withUnstatedContainers(ShareLinkFormWrapperFC, [PageContainer]);
+
+export default ShareLinkFormWrapper;

+ 4 - 9
packages/app/src/components/TrashPageList.jsx

@@ -1,7 +1,6 @@
 import React, { useMemo } from 'react';
 
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 
 import CustomNavAndContents from './CustomNavigation/CustomNavAndContents';
 import { DescendantsPageListForCurrentPath } from './DescendantsPageList';
@@ -9,8 +8,8 @@ import EmptyTrashButton from './EmptyTrashButton';
 import PageListIcon from './Icons/PageListIcon';
 
 
-const TrashPageList = (props) => {
-  const { t } = props;
+const TrashPageList = () => {
+  const { t } = useTranslation();
 
   const navTabMapping = useMemo(() => {
     return {
@@ -34,8 +33,4 @@ const TrashPageList = (props) => {
   );
 };
 
-TrashPageList.propTypes = {
-  t: PropTypes.func.isRequired, //  i18next
-};
-
-export default withTranslation()(TrashPageList);
+export default TrashPageList;

+ 1 - 1
packages/app/src/interfaces/page.ts

@@ -35,7 +35,7 @@ export interface IPage {
 export const PageGrant = {
   GRANT_PUBLIC: 1,
   GRANT_RESTRICTED: 2,
-  GRANT_SPECIFIED: 3,
+  GRANT_SPECIFIED: 3, // DEPRECATED
   GRANT_OWNER: 4,
   GRANT_USER_GROUP: 5,
 };

+ 0 - 6
packages/app/src/server/crowi/index.js

@@ -11,7 +11,6 @@ import mongoose from 'mongoose';
 import pkg from '^/package.json';
 
 import CdnResourcesService from '~/services/cdn-resources-service';
-import InterceptorManager from '~/services/interceptor-manager';
 import Xss from '~/services/xss';
 import loggerFactory from '~/utils/logger';
 import { projectRoot } from '~/utils/project-dir-utils';
@@ -72,7 +71,6 @@ function Crowi() {
   this.pageService = null;
   this.syncPageStatusService = null;
   this.cdnResourcesService = new CdnResourcesService();
-  this.interceptorManager = new InterceptorManager();
   this.slackIntegrationService = null;
   this.inAppNotificationService = null;
   this.activityService = null;
@@ -313,10 +311,6 @@ Crowi.prototype.getSlackLegacy = function() {
   return this.slackLegacy;
 };
 
-Crowi.prototype.getInterceptorManager = function() {
-  return this.interceptorManager;
-};
-
 Crowi.prototype.getGlobalNotificationService = function() {
   return this.globalNotificationService;
 };

+ 6 - 0
packages/app/src/server/models/errors.ts

@@ -10,3 +10,9 @@ export class PathAlreadyExistsError extends ExtensibleCustomError {
   }
 
 }
+
+
+/*
+* User Authentication
+*/
+export class NullUsernameToBeRegisteredError extends ExtensibleCustomError {}

+ 5 - 0
packages/app/src/server/models/external-account.js

@@ -1,5 +1,6 @@
 // disable no-return-await for model functions
 /* eslint-disable no-return-await */
+import { NullUsernameToBeRegisteredError } from '~/server/models/errors';
 
 const debug = require('debug')('growi:models:external-account');
 const mongoose = require('mongoose');
@@ -104,6 +105,10 @@ class ExternalAccount {
           return account;
         }
 
+        if (usernameToBeRegistered == null) {
+          throw new NullUsernameToBeRegisteredError('username_should_not_be_null');
+        }
+
         const User = ExternalAccount.crowi.model('User');
 
         let promise = User.findOne({ username: usernameToBeRegistered });

+ 2 - 1
packages/app/src/server/routes/apiv3/page.js

@@ -246,7 +246,8 @@ module.exports = (crowi) => {
    *                  $ref: '#/components/schemas/Page'
    */
   router.get('/', certifySharedPage, accessTokenParser, loginRequired, validator.getPage, apiV3FormValidator, async(req, res) => {
-    const { pageId, path, user } = req.query;
+    const { user } = req;
+    const { pageId, path } = req.query;
 
     if (pageId == null && path == null) {
       return res.apiv3Err(new ErrorV3('Parameter path or pageId is required.', 'invalid-request'));

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

@@ -78,7 +78,6 @@ module.exports = function(crowi, app) {
   app.get('/login/invited'            , applicationInstalled, login.invited);
   app.post('/login/activateInvited'   , apiLimiter , applicationInstalled, loginFormValidator.inviteRules(), loginFormValidator.inviteValidation, csrf, login.invited);
   app.post('/login'                   , apiLimiter , applicationInstalled, loginFormValidator.loginRules(), loginFormValidator.loginValidation, csrf, loginPassport.loginWithLocal, loginPassport.loginWithLdap, loginPassport.loginFailure);
-  app.post('/login'                   , apiLimiter , applicationInstalled, loginFormValidator.loginRules(), loginFormValidator.loginValidation, csrf, loginPassport.loginWithLocal, loginPassport.loginWithLdap, loginPassport.loginFailure);
 
   app.post('/register'                , apiLimiter , applicationInstalled, registerFormValidator.registerRules(), registerFormValidator.registerValidation, csrf, login.register);
   app.get('/register'                 , applicationInstalled, login.preLogin, login.register);

+ 7 - 1
packages/app/src/server/routes/login-passport.js

@@ -1,3 +1,4 @@
+import { NullUsernameToBeRegisteredError } from '~/server/models/errors';
 import loggerFactory from '~/utils/logger';
 
 /* eslint-disable no-use-before-define */
@@ -112,6 +113,7 @@ module.exports = function(crowi, app) {
     const usernameToBeRegistered = ldapAccountInfo[attrMapUsername];
     const nameToBeRegistered = ldapAccountInfo[attrMapName];
     const mailToBeRegistered = ldapAccountInfo[attrMapMail];
+
     const userInfo = {
       id: ldapAccountId,
       username: usernameToBeRegistered,
@@ -578,7 +580,11 @@ module.exports = function(crowi, app) {
     }
     catch (err) {
       /* eslint-disable no-else-return */
-      if (err.name === 'DuplicatedUsernameException') {
+      if (err instanceof NullUsernameToBeRegisteredError) {
+        req.flash('warningMessage', req.t(`message.${err.message}`));
+        return;
+      }
+      else if (err.name === 'DuplicatedUsernameException') {
         if (isSameEmailTreatedAsIdenticalUser || isSameUsernameTreatedAsIdenticalUser) {
           // associate to existing user
           debug(`ExternalAccount '${userInfo.username}' will be created and bound to the exisiting User account`);

+ 0 - 4
packages/app/src/server/routes/page.js

@@ -153,7 +153,6 @@ module.exports = function(crowi, app) {
   const getToday = require('../util/getToday');
 
   const { configManager, xssService } = crowi;
-  const interceptorManager = crowi.getInterceptorManager();
   const globalNotificationService = crowi.getGlobalNotificationService();
   const userNotificationService = crowi.getUserNotificationService();
 
@@ -404,7 +403,6 @@ module.exports = function(crowi, app) {
 
     await addRenderVarsForPageTree(renderVars, portalPath, req.user);
 
-    await interceptorManager.process('beforeRenderPage', req, res, renderVars);
     return res.render(view, renderVars);
   }
 
@@ -466,7 +464,6 @@ module.exports = function(crowi, app) {
 
     await addRenderVarsForPageTree(renderVars, path, req.user);
 
-    await interceptorManager.process('beforeRenderPage', req, res, renderVars);
     return res.render(view, renderVars);
   }
 
@@ -536,7 +533,6 @@ module.exports = function(crowi, app) {
     addRenderVarsForPage(renderVars, page);
     addRenderVarsForScope(renderVars, page);
 
-    await interceptorManager.process('beforeRenderPage', req, res, renderVars);
     return res.render('layout-growi/shared_page', renderVars);
   };
 

+ 8 - 7
packages/app/src/server/service/page-grant.ts

@@ -408,19 +408,20 @@ class PageGrantService {
     const Page = mongoose.model('Page') as unknown as PageModel;
     const UserGroupRelation = mongoose.model('UserGroupRelation') as any; // TODO: Typescriptize model
 
+    // -- Public only if top page
+    const isOnlyPublicApplicable = isTopPage(page.path);
+    if (isOnlyPublicApplicable) {
+      return {
+        [Page.GRANT_PUBLIC]: null,
+      };
+    }
+
     // Increment an object (type IRecordApplicableGrant)
     // grant is never public, anyone with the link, nor specified
     const data: IRecordApplicableGrant = {
       [Page.GRANT_RESTRICTED]: null, // any page can be restricted
     };
 
-    // -- Public only if top page
-    const isOnlyPublicApplicable = isTopPage(page.path);
-    if (isOnlyPublicApplicable) {
-      data[Page.GRANT_PUBLIC] = null;
-      return data;
-    }
-
     // -- Any grant is allowed if parent is null
     const isAnyGrantApplicable = page.parent == null;
     if (isAnyGrantApplicable) {

+ 18 - 16
packages/app/src/server/service/page.ts

@@ -2638,10 +2638,7 @@ class PageService {
     return isUnique;
   }
 
-  // TODO: use socket to send status to the client
   async normalizeAllPublicPages() {
-    // const socket = this.crowi.socketIoService.getAdminSocket();
-
     let isUnique;
     try {
       isUnique = await this._isPagePathIndexUnique();
@@ -2658,7 +2655,6 @@ class PageService {
       }
       catch (err) {
         logger.error('V5 index normalization failed.', err);
-        // socket.emit('v5IndexNormalizationFailed', { error: err.message });
         throw err;
       }
     }
@@ -2669,7 +2665,6 @@ class PageService {
     }
     catch (err) {
       logger.error('V5 initial miration failed.', err);
-      // socket.emit('v5InitialMirationFailed', { error: err.message });
 
       throw err;
     }
@@ -2713,7 +2708,7 @@ class PageService {
    * @param user To be used to filter pages to update. If null, only public pages will be updated.
    * @returns Promise<void>
    */
-  async normalizeParentRecursively(paths: string[], user: any | null): Promise<number> {
+  async normalizeParentRecursively(paths: string[], user: any | null, shouldEmit = false): Promise<number> {
     const Page = mongoose.model('Page') as unknown as PageModel;
 
     const ancestorPaths = paths.flatMap(p => collectAncestorPaths(p, []));
@@ -2732,7 +2727,7 @@ class PageService {
 
     const grantFiltersByUser: { $or: any[] } = Page.generateGrantCondition(user, userGroups);
 
-    return this._normalizeParentRecursively(pathAndRegExpsToNormalize, ancestorPaths, grantFiltersByUser, user);
+    return this._normalizeParentRecursively(pathAndRegExpsToNormalize, ancestorPaths, grantFiltersByUser, user, shouldEmit);
   }
 
   private buildFilterForNormalizeParentRecursively(pathOrRegExps: (RegExp | string)[], publicPathsToNormalize: string[], grantFiltersByUser: { $or: any[] }) {
@@ -2778,12 +2773,19 @@ class PageService {
   }
 
   private async _normalizeParentRecursively(
-      pathOrRegExps: (RegExp | string)[], publicPathsToNormalize: string[], grantFiltersByUser: { $or: any[] }, user, count = 0, skiped = 0, isFirst = true,
+      pathOrRegExps: (RegExp | string)[],
+      publicPathsToNormalize: string[],
+      grantFiltersByUser: { $or: any[] },
+      user,
+      shouldEmit = false,
+      count = 0,
+      skiped = 0,
+      isFirst = true,
   ): Promise<number> {
     const BATCH_SIZE = 100;
     const PAGES_LIMIT = 1000;
 
-    const socket = this.crowi.socketIoService.getAdminSocket();
+    const socket = shouldEmit ? this.crowi.socketIoService.getAdminSocket() : null;
 
     const Page = mongoose.model('Page') as unknown as PageModel;
     const { PageQueryBuilder } = Page;
@@ -2805,7 +2807,7 @@ class PageService {
     // Limit pages to get
     const total = await Page.countDocuments(matchFilter);
     if (isFirst) {
-      socket.emit(SocketEventName.PMStarted, { total });
+      socket?.emit(SocketEventName.PMStarted, { total });
     }
     if (total > PAGES_LIMIT) {
       baseAggregation = baseAggregation.limit(Math.floor(total * 0.3));
@@ -2912,13 +2914,13 @@ class PageService {
           nextSkiped += res.result.writeErrors.length;
           logger.info(`Page migration processing: (migratedPages=${res.result.nModified})`);
 
-          socket.emit(SocketEventName.PMMigrating, { count: nextCount });
-          socket.emit(SocketEventName.PMErrorCount, { skip: nextSkiped });
+          socket?.emit(SocketEventName.PMMigrating, { count: nextCount });
+          socket?.emit(SocketEventName.PMErrorCount, { skip: nextSkiped });
 
           // Throw if any error is found
           if (res.result.writeErrors.length > 0) {
             logger.error('Failed to migrate some pages', res.result.writeErrors);
-            socket.emit(SocketEventName.PMEnded, { isSucceeded: false });
+            socket?.emit(SocketEventName.PMEnded, { isSucceeded: false });
             throw Error('Failed to migrate some pages');
           }
 
@@ -2926,7 +2928,7 @@ class PageService {
           if (res.result.nModified === 0 && res.result.nMatched === 0) {
             shouldContinue = false;
             logger.error('Migration is unable to continue', 'parentPaths:', parentPaths, 'bulkWriteResult:', res);
-            socket.emit(SocketEventName.PMEnded, { isSucceeded: false });
+            socket?.emit(SocketEventName.PMEnded, { isSucceeded: false });
           }
         }
         catch (err) {
@@ -2948,11 +2950,11 @@ class PageService {
     await streamToPromise(migratePagesStream);
 
     if (await Page.exists(matchFilter) && shouldContinue) {
-      return this._normalizeParentRecursively(pathOrRegExps, publicPathsToNormalize, grantFiltersByUser, user, nextCount, nextSkiped, false);
+      return this._normalizeParentRecursively(pathOrRegExps, publicPathsToNormalize, grantFiltersByUser, user, shouldEmit, nextCount, nextSkiped, false);
     }
 
     // End
-    socket.emit(SocketEventName.PMEnded, { isSucceeded: true });
+    socket?.emit(SocketEventName.PMEnded, { isSucceeded: true });
 
     return nextCount;
   }

+ 4 - 3
packages/app/src/server/views/installer.html

@@ -31,7 +31,7 @@
 
   {{ cdnStyleTagsByGroup('basis') }}
 
-  <script src="{{ webpack_asset('js/nologin.js') }}" defer></script>
+  <script src="{{ webpack_asset('js/installer.js') }}" defer></script>
 
 </head>
 {% endblock %}
@@ -43,6 +43,8 @@
   data-csrftoken="{{ csrf() }}"
  >
 
+<div id="growi-context-extractor"></div>
+
 <div id="wrapper">
 
   <!-- Page Content -->
@@ -72,8 +74,7 @@
           <div id="installer-form-container"
             data-user-name="{{ req.body.registerForm.username }}"
             data-name="{{ req.body.registerForm.name }}"
-            data-email="{{ req.body.registerForm.email }}"
-            data-csrf="{{ csrf() }}">
+            data-email="{{ req.body.registerForm.email }}">
           </div>
         </div>
       </div>{# /.row #}

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

@@ -12,6 +12,10 @@ import { useStaticSWR } from './use-static-swr';
 type Nullable<T> = T | null;
 
 
+export const useCsrfToken = (initialData?: string): SWRResponse<string, Error> => {
+  return useStaticSWR<string, Error>('csrfToken', initialData);
+};
+
 export const useSiteUrl = (initialData?: string): SWRResponse<string, Error> => {
   return useStaticSWR<string, Error>('siteUrl', initialData);
 };

+ 11 - 7
packages/app/src/stores/websocket.tsx

@@ -33,15 +33,19 @@ export const useGlobalSocket = (): SWRResponse<Socket, Error> => {
 /*
  * Global Admin Socket
  */
-export const useSetupGlobalAdminSocket = (): SWRResponse<Socket, Error> => {
-  const socket = io(GLOBAL_ADMIN_SOCKET_NS, {
-    transports: ['websocket'],
-  });
+export const useSetupGlobalAdminSocket = (shouldInit: boolean): SWRResponse<Socket, Error> => {
+  let socket: Socket | undefined;
 
-  socket.on('error', (err) => { logger.error(err) });
-  socket.on('connect_error', (err) => { logger.error('Failed to connect with websocket.', err) });
+  if (shouldInit) {
+    socket = io(GLOBAL_ADMIN_SOCKET_NS, {
+      transports: ['websocket'],
+    });
+
+    socket.on('error', (err) => { logger.error(err) });
+    socket.on('connect_error', (err) => { logger.error('Failed to connect with websocket.', err) });
+  }
 
-  return useStaticSWR(GLOBAL_ADMIN_SOCKET_KEY, socket);
+  return useStaticSWR(shouldInit ? GLOBAL_ADMIN_SOCKET_KEY : null, socket);
 };
 
 export const useGlobalAdminSocket = (): SWRResponse<Socket, Error> => {

+ 6 - 0
packages/app/src/styles/_navbar.scss

@@ -63,6 +63,12 @@
       }
     }
   }
+
+  .grw-notification-dropdown {
+    .dropdown-menu {
+      max-width: 70vw;
+    }
+  }
 }
 
 .grw-navbar-bottom {

+ 5 - 1
packages/app/src/styles/theme/_apply-colors-light.scss

@@ -178,7 +178,11 @@ $dropdown-link-active-bg: $bgcolor-dropdown-link-active;
 .grw-sidebar {
   // List
   @include override-list-group-item($color-list, $bgcolor-sidebar-list-group, $color-list-hover, $bgcolor-list-hover, $color-list-active, $bgcolor-list-active);
-
+  // sidebar-centent-bg
+  .grw-navigation-wrap {
+    // Drop a shadow on the light theme. The dark theme makes '$ bgcolor-sidebar-context' brighter than the body.
+    box-shadow: 0px 0px 3px rgba(black, 0.24);
+  }
   // Pagetree
   .grw-pagetree {
     @include override-list-group-item-for-pagetree(

+ 1 - 1
packages/app/src/styles/theme/blackboard.scss

@@ -60,7 +60,7 @@ html[dark] {
   $color-resize-button-hover: $color-global;
   $bgcolor-resize-button-hover: darken($bgcolor-resize-button, 5%);
   // Sidebar contents
-  $bgcolor-sidebar-context: $subthemecolor;
+  $bgcolor-sidebar-context: lighten($subthemecolor, 8%);
   $color-sidebar-context: $color-global;
   // Sidebar list group
   // $bgcolor-sidebar-list-group: #; // optional

+ 1 - 1
packages/app/src/styles/theme/christmas.scss

@@ -85,7 +85,7 @@ html[dark] {
   $color-resize-button-hover: $color-reversal;
   $bgcolor-resize-button-hover: lighten($bgcolor-resize-button, 5%);
   $color-sidebar-context: $linktext;
-  $bgcolor-sidebar-context: #f4f6fc;
+  $bgcolor-sidebar-context: #f4fcf6;
   // Sidebar list group
   $bgcolor-sidebar-list-group: #fafbff; // optional
 

+ 2 - 2
packages/app/src/styles/theme/fire-red.scss

@@ -56,7 +56,7 @@ html[light] {
   $bgcolor-resize-button-hover: lighten($bgcolor-resize-button, 5%);
   // Sidebar contents
   $color-sidebar-context: $color-global;
-  $bgcolor-sidebar-context: #ebebeb;
+  $bgcolor-sidebar-context: #ececec;
   // Sidebar list group
   // $bgcolor-sidebar-list-group: #; // optional
 
@@ -152,7 +152,7 @@ html[dark] {
   $color-resize-button-hover: $color-global;
   $bgcolor-resize-button-hover: darken($bgcolor-resize-button, 5%);
   // Sidebar contents
-  $bgcolor-sidebar-context: #2e2e2e;
+  $bgcolor-sidebar-context: #413f3f;
   $color-sidebar-context: $color-global;
   // Sidebar list group
   // $bgcolor-sidebar-list-group: #; // optional

+ 2 - 2
packages/app/src/styles/theme/future.scss

@@ -55,8 +55,8 @@ html[dark] {
   $text-shadow-sidebar-nav-item-active: 0px 0px 10px #969494; // optional
 
   // Sidebar contents
-  $color-sidebar-context: #00c2c4;
-  $bgcolor-sidebar-context: #020b0b;
+  $color-sidebar-context: #2cfbff;
+  $bgcolor-sidebar-context: #184040;
 
   // Sidebar list group
   $bgcolor-sidebar-list-group: #162126; // optional

+ 1 - 1
packages/app/src/styles/theme/halloween.scss

@@ -80,7 +80,7 @@ html[dark] {
 
   // Sidebar contents
   $color-sidebar-context: #aa97cb;
-  $bgcolor-sidebar-context: #1d2126;
+  $bgcolor-sidebar-context: #302b3c;
 
   // Sidebar list group
   $bgcolor-sidebar-list-group: #2c2926; // optional

+ 1 - 1
packages/app/src/styles/theme/hufflepuff.scss

@@ -216,7 +216,7 @@ html[dark] {
   $bgcolor-resize-button-hover: darken($bgcolor-resize-button, 7%);
   // Sidebar contents
   $color-sidebar-context: $color-global;
-  $bgcolor-sidebar-context: $subthemecolor;
+  $bgcolor-sidebar-context: lighten($themedark, 5%);
   // Sidebar list group
   $bgcolor-sidebar-list-group: lighten($subthemecolor, 5%);
 

Некоторые файлы не были показаны из-за большого количества измененных файлов