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

Merge branch 'imprv/integrate-convert-to-useTranslation' of https://github.com/weseek/growi into imprv/97302-convert-12-files

Kami-jo 3 лет назад
Родитель
Сommit
7a61a0238e
100 измененных файлов с 2015 добавлено и 2021 удалено
  1. 1 0
      packages/app/config/webpack.common.js
  2. 1 0
      packages/app/resource/locales/en_US/translation.json
  3. 1 0
      packages/app/resource/locales/ja_JP/translation.json
  4. 2 1
      packages/app/resource/locales/zh_CN/translation.json
  5. 2 0
      packages/app/src/client/base.jsx
  6. 60 0
      packages/app/src/client/installer.jsx
  7. 2 1
      packages/app/src/client/legacy/crowi.js
  8. 67 65
      packages/app/src/client/nologin.jsx
  9. 0 15
      packages/app/src/client/services/AppContainer.js
  10. 2 3
      packages/app/src/client/services/CommentContainer.js
  11. 8 1
      packages/app/src/client/services/ContextExtractor.tsx
  12. 5 3
      packages/app/src/client/services/PageContainer.js
  13. 7 0
      packages/app/src/client/util/apiv1-client.ts
  14. 12 3
      packages/app/src/client/util/apiv3-client.ts
  15. 2 1
      packages/app/src/client/util/markdown-it/toc-and-anchor.js
  16. 2 2
      packages/app/src/client/util/reveal/plugins/growi-renderer.js
  17. 39 41
      packages/app/src/components/Admin/AdminHome/InstalledPluginTable.jsx
  18. 0 59
      packages/app/src/components/Admin/AdminHome/SystemInfomationTable.jsx
  19. 53 0
      packages/app/src/components/Admin/AdminHome/SystemInfomationTable.tsx
  20. 139 145
      packages/app/src/components/Admin/App/AppSetting.jsx
  21. 0 113
      packages/app/src/components/Admin/App/AppSettingsPageContents.jsx
  22. 108 0
      packages/app/src/components/Admin/App/AppSettingsPageContents.tsx
  23. 1 4
      packages/app/src/components/Admin/App/AwsSetting.jsx
  24. 20 24
      packages/app/src/components/Admin/App/FileUploadSetting.tsx
  25. 10 11
      packages/app/src/components/Admin/App/GcsSettings.jsx
  26. 16 18
      packages/app/src/components/Admin/App/MailSetting.tsx
  27. 0 79
      packages/app/src/components/Admin/App/PluginSetting.jsx
  28. 66 0
      packages/app/src/components/Admin/App/PluginSetting.tsx
  29. 10 16
      packages/app/src/components/Admin/App/SesSetting.tsx
  30. 0 105
      packages/app/src/components/Admin/App/SiteUrlSetting.jsx
  31. 93 0
      packages/app/src/components/Admin/App/SiteUrlSetting.tsx
  32. 14 17
      packages/app/src/components/Admin/App/SmtpSetting.tsx
  33. 0 79
      packages/app/src/components/Admin/Customize/CustomizeCssSetting.jsx
  34. 68 0
      packages/app/src/components/Admin/Customize/CustomizeCssSetting.tsx
  35. 0 39
      packages/app/src/components/Admin/Customize/CustomizeFunctionOption.jsx
  36. 37 0
      packages/app/src/components/Admin/Customize/CustomizeFunctionOption.tsx
  37. 0 174
      packages/app/src/components/Admin/Customize/CustomizeFunctionSetting.jsx
  38. 163 0
      packages/app/src/components/Admin/Customize/CustomizeFunctionSetting.tsx
  39. 0 89
      packages/app/src/components/Admin/Customize/CustomizeHeaderSetting.jsx
  40. 76 0
      packages/app/src/components/Admin/Customize/CustomizeHeaderSetting.tsx
  41. 0 156
      packages/app/src/components/Admin/Customize/CustomizeHighlightSetting.jsx
  42. 145 0
      packages/app/src/components/Admin/Customize/CustomizeHighlightSetting.tsx
  43. 4 12
      packages/app/src/components/Admin/Customize/CustomizeLayoutSetting.tsx
  44. 0 120
      packages/app/src/components/Admin/Customize/CustomizeScriptSetting.jsx
  45. 107 0
      packages/app/src/components/Admin/Customize/CustomizeScriptSetting.tsx
  46. 78 80
      packages/app/src/components/Admin/Customize/CustomizeThemeOptions.jsx
  47. 0 72
      packages/app/src/components/Admin/Customize/CustomizeThemeSetting.jsx
  48. 58 0
      packages/app/src/components/Admin/Customize/CustomizeThemeSetting.tsx
  49. 2 3
      packages/app/src/components/Admin/Customize/PagingSizeUncontrolledDropdown.jsx
  50. 8 3
      packages/app/src/components/Admin/ElasticsearchManagement/ElasticsearchManagement.jsx
  51. 8 8
      packages/app/src/components/Admin/ElasticsearchManagement/StatusTable.jsx
  52. 0 66
      packages/app/src/components/Admin/ExportArchiveData/ArchiveFilesTable.jsx
  53. 51 0
      packages/app/src/components/Admin/ExportArchiveData/ArchiveFilesTable.tsx
  54. 0 46
      packages/app/src/components/Admin/ExportArchiveData/ArchiveFilesTableMenu.jsx
  55. 33 0
      packages/app/src/components/Admin/ExportArchiveData/ArchiveFilesTableMenu.tsx
  56. 7 9
      packages/app/src/components/Admin/ExportArchiveData/SelectCollectionsModal.jsx
  57. 0 50
      packages/app/src/components/Admin/ImportData/GrowiArchive/ErrorViewer.jsx
  58. 34 0
      packages/app/src/components/Admin/ImportData/GrowiArchive/ErrorViewer.tsx
  59. 8 8
      packages/app/src/components/Admin/ImportData/GrowiArchive/ImportCollectionConfigurationModal.jsx
  60. 1 4
      packages/app/src/components/Admin/ImportData/GrowiArchive/ImportCollectionItem.jsx
  61. 10 6
      packages/app/src/components/Admin/ImportData/GrowiArchive/ImportForm.jsx
  62. 9 13
      packages/app/src/components/Admin/ImportData/GrowiArchive/UploadForm.jsx
  63. 7 9
      packages/app/src/components/Admin/ImportData/GrowiArchiveSection.jsx
  64. 14 6
      packages/app/src/components/Admin/ImportData/ImportDataPageContents.jsx
  65. 13 8
      packages/app/src/components/Admin/LegacySlackIntegration/SlackConfiguration.jsx
  66. 14 8
      packages/app/src/components/Admin/MarkdownSetting/PresentationForm.jsx
  67. 14 6
      packages/app/src/components/Admin/MarkdownSetting/WhiteListInput.jsx
  68. 14 7
      packages/app/src/components/Admin/MarkdownSetting/XssForm.jsx
  69. 14 7
      packages/app/src/components/Admin/Notification/GlobalNotification.jsx
  70. 10 4
      packages/app/src/components/Admin/Notification/GlobalNotificationList.jsx
  71. 11 4
      packages/app/src/components/Admin/Notification/ManageGlobalNotification.jsx
  72. 10 3
      packages/app/src/components/Admin/Notification/NotificationDeleteModal.jsx
  73. 9 2
      packages/app/src/components/Admin/Notification/TriggerEventCheckBox.jsx
  74. 16 8
      packages/app/src/components/Admin/Notification/UserNotificationRow.jsx
  75. 14 7
      packages/app/src/components/Admin/Notification/UserTriggerNotification.jsx
  76. 13 6
      packages/app/src/components/Admin/Security/BasicSecuritySettingContents.jsx
  77. 10 4
      packages/app/src/components/Admin/Security/DeleteAllShareLinksModal.jsx
  78. 12 17
      packages/app/src/components/Admin/Security/SecurityManagementContents.jsx
  79. 12 5
      packages/app/src/components/Admin/Security/ShareLinkSetting.jsx
  80. 17 8
      packages/app/src/components/Admin/Security/TwitterSecuritySettingContents.jsx
  81. 8 3
      packages/app/src/components/Admin/UserGroupDetail/CheckBoxForSerchUserOption.jsx
  82. 14 11
      packages/app/src/components/Admin/UserManagement.jsx
  83. 17 6
      packages/app/src/components/Admin/Users/InviteUserControl.jsx
  84. 16 9
      packages/app/src/components/Admin/Users/UserRemoveButton.jsx
  85. 9 6
      packages/app/src/components/Common/CountBadge.tsx
  86. 6 9
      packages/app/src/components/Drawio.tsx
  87. 6 3
      packages/app/src/components/InstallerForm.jsx
  88. 11 6
      packages/app/src/components/LoginForm.jsx
  89. 5 16
      packages/app/src/components/Me/ProfileImageSettings.tsx
  90. 1 1
      packages/app/src/components/MyDraftList/Draft.jsx
  91. 2 2
      packages/app/src/components/Page/DisplaySwitcher.tsx
  92. 6 1
      packages/app/src/components/Page/RevisionBody.jsx
  93. 2 2
      packages/app/src/components/Page/RevisionRenderer.jsx
  94. 2 2
      packages/app/src/components/PageComment/Comment.jsx
  95. 5 6
      packages/app/src/components/PageComment/CommentEditor.jsx
  96. 15 20
      packages/app/src/components/PageComment/CommentPreview.jsx
  97. 9 12
      packages/app/src/components/PageEditor.tsx
  98. 5 2
      packages/app/src/components/PageEditor/Preview.tsx
  99. 1 9
      packages/app/src/components/PasswordResetExecutionForm.jsx
  100. 1 12
      packages/app/src/components/PasswordResetRequestForm.jsx

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

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

+ 8 - 1
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);
 

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

+ 39 - 41
packages/app/src/components/Admin/AdminHome/InstalledPluginTable.jsx

@@ -1,57 +1,55 @@
 import React from 'react';
+
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
-import AppContainer from '~/client/services/AppContainer';
 import AdminHomeContainer from '~/client/services/AdminHomeContainer';
 
-class InstalledPluginTable extends React.Component {
-
-  render() {
-    const { t, adminHomeContainer } = this.props;
-
-    const { installedPlugins } = adminHomeContainer.state;
-
-    if (installedPlugins == null) {
-      return <></>;
-    }
-
-    return (
-      <table data-testid="admin-installed-plugin-table" className="table table-bordered">
-        <thead>
-          <tr>
-            <th className="text-center">{t('admin:admin_top.package_name')}</th>
-            <th className="text-center">{t('admin:admin_top.specified_version')}</th>
-            <th className="text-center">{t('admin:admin_top.installed_version')}</th>
-          </tr>
-        </thead>
-        <tbody>
-          {adminHomeContainer.state.installedPlugins.map((plugin) => {
-            return (
-              <tr key={plugin.name}>
-                <td>{plugin.name}</td>
-                <td data-hide-in-vrt className="text-center">{plugin.requiredVersion}</td>
-                <td data-hide-in-vrt className="text-center">{plugin.installedVersion}</td>
-              </tr>
-            );
-          })}
-        </tbody>
-      </table>
-    );
+import { withUnstatedContainers } from '../../UnstatedUtils';
+
+const InstalledPluginTable = (props) => {
+  const { t } = useTranslation();
+  const { adminHomeContainer } = props;
+
+  const { installedPlugins } = adminHomeContainer.state;
+
+  if (installedPlugins == null) {
+    return <></>;
   }
 
-}
+  return (
+    <table data-testid="admin-installed-plugin-table" className="table table-bordered">
+      <thead>
+        <tr>
+          <th className="text-center">{t('admin:admin_top.package_name')}</th>
+          <th className="text-center">{t('admin:admin_top.specified_version')}</th>
+          <th className="text-center">{t('admin:admin_top.installed_version')}</th>
+        </tr>
+      </thead>
+      <tbody>
+        {adminHomeContainer.state.installedPlugins.map((plugin) => {
+          return (
+            <tr key={plugin.name}>
+              <td>{plugin.name}</td>
+              <td data-hide-in-vrt className="text-center">{plugin.requiredVersion}</td>
+              <td data-hide-in-vrt className="text-center">{plugin.installedVersion}</td>
+            </tr>
+          );
+        })}
+      </tbody>
+    </table>
+  );
+
+};
 
 InstalledPluginTable.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminHomeContainer: PropTypes.instanceOf(AdminHomeContainer).isRequired,
 };
 
+
 /**
  * Wrapper component for using unstated
  */
-const InstalledPluginTableWrapper = withUnstatedContainers(InstalledPluginTable, [AppContainer, AdminHomeContainer]);
+const InstalledPluginTableWrapper = withUnstatedContainers(InstalledPluginTable, [AdminHomeContainer]);
 
-export default withTranslation()(InstalledPluginTableWrapper);
+export default InstalledPluginTableWrapper;

+ 0 - 59
packages/app/src/components/Admin/AdminHome/SystemInfomationTable.jsx

@@ -1,59 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-import AppContainer from '~/client/services/AppContainer';
-import AdminHomeContainer from '~/client/services/AdminHomeContainer';
-
-class SystemInformationTable extends React.Component {
-
-  render() {
-    const { adminHomeContainer } = this.props;
-
-    const {
-      growiVersion, nodeVersion, npmVersion, yarnVersion,
-    } = adminHomeContainer.state;
-
-    if (growiVersion == null || nodeVersion == null || npmVersion == null || yarnVersion == null) {
-      return <></>;
-    }
-
-    return (
-      <table data-testid="admin-system-information-table" className="table table-bordered">
-        <tbody>
-          <tr>
-            <th>GROWI</th>
-            <td data-hide-in-vrt>{ growiVersion }</td>
-          </tr>
-          <tr>
-            <th>node.js</th>
-            <td>{ nodeVersion }</td>
-          </tr>
-          <tr>
-            <th>npm</th>
-            <td>{ npmVersion }</td>
-          </tr>
-          <tr>
-            <th>yarn</th>
-            <td>{ yarnVersion }</td>
-          </tr>
-        </tbody>
-      </table>
-    );
-  }
-
-}
-
-SystemInformationTable.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminHomeContainer: PropTypes.instanceOf(AdminHomeContainer).isRequired,
-};
-
-/**
- * Wrapper component for using unstated
- */
-const SystemInformationTableWrapper = withUnstatedContainers(SystemInformationTable, [AppContainer, AdminHomeContainer]);
-
-export default withTranslation()(SystemInformationTableWrapper);

+ 53 - 0
packages/app/src/components/Admin/AdminHome/SystemInfomationTable.tsx

@@ -0,0 +1,53 @@
+import React from 'react';
+
+import AdminHomeContainer from '~/client/services/AdminHomeContainer';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
+
+
+type Props = {
+  adminHomeContainer: AdminHomeContainer,
+}
+
+const SystemInformationTable = (props: Props) => {
+  const { adminHomeContainer } = props;
+
+  const {
+    growiVersion, nodeVersion, npmVersion, yarnVersion,
+  } = adminHomeContainer.state;
+
+  if (growiVersion == null || nodeVersion == null || npmVersion == null || yarnVersion == null) {
+    return <></>;
+  }
+
+  return (
+    <table data-testid="admin-system-information-table" className="table table-bordered">
+      <tbody>
+        <tr>
+          <th>GROWI</th>
+          <td data-hide-in-vrt>{ growiVersion }</td>
+        </tr>
+        <tr>
+          <th>node.js</th>
+          <td>{ nodeVersion }</td>
+        </tr>
+        <tr>
+          <th>npm</th>
+          <td>{ npmVersion }</td>
+        </tr>
+        <tr>
+          <th>yarn</th>
+          <td>{ yarnVersion }</td>
+        </tr>
+      </tbody>
+    </table>
+  );
+
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const SystemInformationTableWrapper = withUnstatedContainers(SystemInformationTable, [AdminHomeContainer]);
+
+export default SystemInformationTableWrapper;

+ 139 - 145
packages/app/src/components/Admin/App/AppSetting.jsx

@@ -1,29 +1,25 @@
-import React from 'react';
+import React, { useCallback } from 'react';
+
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-import loggerFactory from '~/utils/logger';
+import { useTranslation } from 'react-i18next';
 
+import AdminAppContainer from '~/client/services/AdminAppContainer';
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { localeMetadatas } from '~/client/util/i18n';
+import loggerFactory from '~/utils/logger';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
-import AdminAppContainer from '~/client/services/AdminAppContainer';
+import { withUnstatedContainers } from '../../UnstatedUtils';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 const logger = loggerFactory('growi:appSettings');
 
-class AppSetting extends React.Component {
-
-  constructor(props) {
-    super(props);
 
-    this.submitHandler = this.submitHandler.bind(this);
-  }
-
-  async submitHandler() {
-    const { t, adminAppContainer } = this.props;
+const AppSetting = (props) => {
+  const { adminAppContainer } = props;
+  const { t } = useTranslation();
 
+  const submitHandler = useCallback(async() => {
     try {
       await adminAppContainer.updateAppSettingHandler();
       toastSuccess(t('toaster.update_successed', { target: t('App Settings') }));
@@ -32,150 +28,148 @@ class AppSetting extends React.Component {
       toastError(err);
       logger.error(err);
     }
-  }
+  }, [adminAppContainer, t]);
+
+
+  return (
+    <React.Fragment>
+      <div className="form-group row">
+        <label className="text-left text-md-right col-md-3 col-form-label">{t('admin:app_setting.site_name')}</label>
+        <div className="col-md-6">
+          <input
+            className="form-control"
+            type="text"
+            defaultValue={adminAppContainer.state.title || ''}
+            onChange={(e) => {
+              adminAppContainer.changeTitle(e.target.value);
+            }}
+            placeholder="GROWI"
+          />
+          <p className="form-text text-muted">{t('admin:app_setting.sitename_change')}</p>
+        </div>
+      </div>
+
+      <div className="row form-group mb-5">
+        <label
+          className="text-left text-md-right col-md-3 col-form-label"
+        >
+          {t('admin:app_setting.confidential_name')}
+        </label>
+        <div className="col-md-6">
+          <input
+            className="form-control"
+            type="text"
+            defaultValue={adminAppContainer.state.confidential || ''}
+            onChange={(e) => {
+              adminAppContainer.changeConfidential(e.target.value);
+            }}
+            placeholder={t('admin:app_setting.confidential_example')}
+          />
+          <p className="form-text text-muted">{t('admin:app_setting.header_content')}</p>
+        </div>
+      </div>
+
+      <div className="row form-group mb-5">
+        <label
+          className="text-left text-md-right col-md-3 col-form-label"
+        >
+          {t('admin:app_setting.default_language')}
+        </label>
+        <div className="col-md-6 py-2">
+          {
+            localeMetadatas.map(meta => (
+              <div key={meta.id} className="custom-control custom-radio custom-control-inline">
+                <input
+                  type="radio"
+                  id={`radioLang${meta.id}`}
+                  className="custom-control-input"
+                  name="globalLang"
+                  value={meta.id}
+                  checked={adminAppContainer.state.globalLang === meta.id}
+                  onChange={(e) => {
+                    adminAppContainer.changeGlobalLang(e.target.value);
+                  }}
+                />
+                <label className="custom-control-label" htmlFor={`radioLang${meta.id}`}>{meta.displayName}</label>
+              </div>
+            ))
+          }
+        </div>
+      </div>
 
-  render() {
-    const { t, adminAppContainer } = this.props;
+      <div className="row form-group mb-5">
+        <label
+          className="text-left text-md-right col-md-3 col-form-label"
+        >
+          {t('admin:app_setting.default_mail_visibility')}
+        </label>
+        <div className="col-md-6 py-2">
 
-    return (
-      <React.Fragment>
-        <div className="form-group row">
-          <label className="text-left text-md-right col-md-3 col-form-label">{t('admin:app_setting.site_name')}</label>
-          <div className="col-md-6">
+          <div className="custom-control custom-radio custom-control-inline">
             <input
-              className="form-control"
-              type="text"
-              defaultValue={adminAppContainer.state.title || ''}
-              onChange={(e) => {
-                adminAppContainer.changeTitle(e.target.value);
-              }}
-              placeholder="GROWI"
+              type="radio"
+              id="radio-email-show"
+              className="custom-control-input"
+              name="mailVisibility"
+              checked={adminAppContainer.state.isEmailPublishedForNewUser === true}
+              onChange={() => { adminAppContainer.changeIsEmailPublishedForNewUserShow(true) }}
             />
-            <p className="form-text text-muted">{t('admin:app_setting.sitename_change')}</p>
+            <label className="custom-control-label" htmlFor="radio-email-show">{t('Show')}</label>
           </div>
-        </div>
 
-        <div className="row form-group mb-5">
-          <label
-            className="text-left text-md-right col-md-3 col-form-label"
-          >
-            {t('admin:app_setting.confidential_name')}
-          </label>
-          <div className="col-md-6">
+          <div className="custom-control custom-radio custom-control-inline">
             <input
-              className="form-control"
-              type="text"
-              defaultValue={adminAppContainer.state.confidential || ''}
-              onChange={(e) => {
-                adminAppContainer.changeConfidential(e.target.value);
-              }}
-              placeholder={t('admin:app_setting.confidential_example')}
+              type="radio"
+              id="radio-email-hide"
+              className="custom-control-input"
+              name="mailVisibility"
+              checked={adminAppContainer.state.isEmailPublishedForNewUser === false}
+              onChange={() => { adminAppContainer.changeIsEmailPublishedForNewUserShow(false) }}
             />
-            <p className="form-text text-muted">{t('admin:app_setting.header_content')}</p>
+            <label className="custom-control-label" htmlFor="radio-email-hide">{t('Hide')}</label>
           </div>
-        </div>
 
-        <div className="row form-group mb-5">
-          <label
-            className="text-left text-md-right col-md-3 col-form-label"
-          >
-            {t('admin:app_setting.default_language')}
-          </label>
-          <div className="col-md-6 py-2">
-            {
-              localeMetadatas.map(meta => (
-                <div key={meta.id} className="custom-control custom-radio custom-control-inline">
-                  <input
-                    type="radio"
-                    id={`radioLang${meta.id}`}
-                    className="custom-control-input"
-                    name="globalLang"
-                    value={meta.id}
-                    checked={adminAppContainer.state.globalLang === meta.id}
-                    onChange={(e) => {
-                      adminAppContainer.changeGlobalLang(e.target.value);
-                    }}
-                  />
-                  <label className="custom-control-label" htmlFor={`radioLang${meta.id}`}>{meta.displayName}</label>
-                </div>
-              ))
-            }
-          </div>
         </div>
-
-        <div className="row form-group mb-5">
-          <label
-            className="text-left text-md-right col-md-3 col-form-label"
-          >
-            {t('admin:app_setting.default_mail_visibility')}
-          </label>
-          <div className="col-md-6 py-2">
-
-            <div className="custom-control custom-radio custom-control-inline">
-              <input
-                type="radio"
-                id="radio-email-show"
-                className="custom-control-input"
-                name="mailVisibility"
-                checked={adminAppContainer.state.isEmailPublishedForNewUser === true}
-                onChange={() => { adminAppContainer.changeIsEmailPublishedForNewUserShow(true) }}
-              />
-              <label className="custom-control-label" htmlFor="radio-email-show">{t('Show')}</label>
-            </div>
-
-            <div className="custom-control custom-radio custom-control-inline">
-              <input
-                type="radio"
-                id="radio-email-hide"
-                className="custom-control-input"
-                name="mailVisibility"
-                checked={adminAppContainer.state.isEmailPublishedForNewUser === false}
-                onChange={() => { adminAppContainer.changeIsEmailPublishedForNewUserShow(false) }}
-              />
-              <label className="custom-control-label" htmlFor="radio-email-hide">{t('Hide')}</label>
-            </div>
-
+      </div>
+
+      <div className="row form-group mb-5">
+        <label
+          className="text-left text-md-right col-md-3 col-form-label"
+        >
+          {/* {t('admin:app_setting.file_uploading')} */}
+        </label>
+        <div className="col-md-6">
+          <div className="custom-control custom-checkbox custom-checkbox-info">
+            <input
+              type="checkbox"
+              id="cbFileUpload"
+              className="custom-control-input"
+              name="fileUpload"
+              checked={adminAppContainer.state.fileUpload}
+              onChange={(e) => {
+                adminAppContainer.changeFileUpload(e.target.checked);
+              }}
+            />
+            <label
+              className="custom-control-label"
+              htmlFor="cbFileUpload"
+            >
+              {t('admin:app_setting.enable_files_except_image')}
+            </label>
           </div>
-        </div>
 
-        <div className="row form-group mb-5">
-          <label
-            className="text-left text-md-right col-md-3 col-form-label"
-          >
-            {t('admin:app_setting.file_uploading')}
-          </label>
-          <div className="col-md-6">
-            <div className="custom-control custom-checkbox custom-checkbox-info">
-              <input
-                type="checkbox"
-                id="cbFileUpload"
-                className="custom-control-input"
-                name="fileUpload"
-                checked={adminAppContainer.state.fileUpload}
-                onChange={(e) => {
-                  adminAppContainer.changeFileUpload(e.target.checked);
-                }}
-              />
-              <label
-                className="custom-control-label"
-                htmlFor="cbFileUpload"
-              >
-                {t('admin:app_setting.enable_files_except_image')}
-              </label>
-            </div>
-
-            <p className="form-text text-muted">
-              {t('admin:app_setting.attach_enable')}
-            </p>
-          </div>
+          <p className="form-text text-muted">
+            {t('admin:app_setting.attach_enable')}
+          </p>
         </div>
+      </div>
+
+      <AdminUpdateButtonRow onClick={submitHandler} disabled={adminAppContainer.state.retrieveError != null} />
+    </React.Fragment>
+  );
 
-        <AdminUpdateButtonRow onClick={this.submitHandler} disabled={adminAppContainer.state.retrieveError != null} />
-      </React.Fragment>
-    );
-  }
+};
 
-}
 
 /**
  * Wrapper component for using unstated
@@ -183,8 +177,8 @@ class AppSetting extends React.Component {
 const AppSettingWrapper = withUnstatedContainers(AppSetting, [AdminAppContainer]);
 
 AppSetting.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
   adminAppContainer: PropTypes.instanceOf(AdminAppContainer).isRequired,
 };
 
-export default withTranslation()(AppSettingWrapper);
+
+export default AppSettingWrapper;

+ 0 - 113
packages/app/src/components/Admin/App/AppSettingsPageContents.jsx

@@ -1,113 +0,0 @@
-import React from 'react';
-import { withTranslation } from 'react-i18next';
-import PropTypes from 'prop-types';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-import AppSetting from './AppSetting';
-import SiteUrlSetting from './SiteUrlSetting';
-import MailSetting from './MailSetting';
-import PluginSetting from './PluginSetting';
-import FileUploadSetting from './FileUploadSetting';
-import V5PageMigration from './V5PageMigration';
-import MaintenanceMode from './MaintenanceMode';
-
-import AdminAppContainer from '~/client/services/AdminAppContainer';
-
-class AppSettingsPageContents extends React.Component {
-
-  render() {
-    const { t, adminAppContainer } = this.props;
-    const { isV5Compatible } = adminAppContainer.state;
-
-    return (
-      <div data-testid="admin-app-settings">
-        {
-          // Alert message will be displayed in case that the GROWI is under maintenance
-          adminAppContainer.state.isMaintenanceMode && (
-            <div className="alert alert-danger alert-link" role="alert">
-              <h3 className="alert-heading">
-                {t('admin:maintenance_mode.maintenance_mode')}
-              </h3>
-              <p>
-                {t('admin:maintenance_mode.description')}
-              </p>
-              <hr />
-              <a className="btn-link" href="#maintenance-mode" rel="noopener noreferrer">
-                <i className="fa fa-fw fa-arrow-down ml-1" aria-hidden="true"></i>
-                <strong>{t('admin:maintenance_mode.end_maintenance_mode')}</strong>
-              </a>
-            </div>
-          )
-        }
-        {
-          !isV5Compatible
-          && (
-            <div className="row">
-              <div className="col-lg-12">
-                <h2 className="admin-setting-header">{t('V5 Page Migration')}</h2>
-                <V5PageMigration />
-              </div>
-            </div>
-          )
-        }
-
-        <div className="row">
-          <div className="col-lg-12">
-            <h2 className="admin-setting-header">{t('App Settings')}</h2>
-            <AppSetting />
-          </div>
-        </div>
-
-        <div className="row mt-5">
-          <div className="col-lg-12">
-            <h2 className="admin-setting-header">{t('Site URL settings')}</h2>
-            <SiteUrlSetting />
-          </div>
-        </div>
-
-        <div className="row mt-5">
-          <div className="col-lg-12">
-            <h2 className="admin-setting-header" id="mail-settings">{t('admin:app_setting.mail_settings')}</h2>
-            <MailSetting />
-          </div>
-        </div>
-
-        <div className="row mt-5">
-          <div className="col-lg-12">
-            <h2 className="admin-setting-header">{t('admin:app_setting.file_upload_settings')}</h2>
-            <FileUploadSetting />
-          </div>
-        </div>
-
-        <div className="row mt-5">
-          <div className="col-lg-12">
-            <h2 className="admin-setting-header">{t('admin:app_setting.plugin_settings')}</h2>
-            <PluginSetting />
-          </div>
-        </div>
-
-        <div className="row">
-          <div className="col-lg-12">
-            <h2 className="admin-setting-header" id="maintenance-mode">{t('admin:maintenance_mode.maintenance_mode')}</h2>
-            <MaintenanceMode />
-          </div>
-        </div>
-
-      </div>
-
-    );
-  }
-
-}
-
-/**
- * Wrapper component for using unstated
- */
-const AppSettingsPageContentsWrapper = withUnstatedContainers(AppSettingsPageContents, [AdminAppContainer]);
-
-AppSettingsPageContents.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  adminAppContainer: PropTypes.instanceOf(AdminAppContainer).isRequired,
-};
-
-export default withTranslation()(AppSettingsPageContentsWrapper);

+ 108 - 0
packages/app/src/components/Admin/App/AppSettingsPageContents.tsx

@@ -0,0 +1,108 @@
+import React from 'react';
+
+import { useTranslation } from 'react-i18next';
+
+import AdminAppContainer from '~/client/services/AdminAppContainer';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
+
+import AppSetting from './AppSetting';
+import FileUploadSetting from './FileUploadSetting';
+import MailSetting from './MailSetting';
+import MaintenanceMode from './MaintenanceMode';
+import PluginSetting from './PluginSetting';
+import SiteUrlSetting from './SiteUrlSetting';
+import V5PageMigration from './V5PageMigration';
+
+type Props = {
+  adminAppContainer: AdminAppContainer,
+}
+
+const AppSettingsPageContents = (props: Props) => {
+  const { t } = useTranslation();
+  const { adminAppContainer } = props;
+  const { isV5Compatible } = adminAppContainer.state;
+
+  return (
+    <div data-testid="admin-app-settings">
+      {
+        // Alert message will be displayed in case that the GROWI is under maintenance
+        adminAppContainer.state.isMaintenanceMode && (
+          <div className="alert alert-danger alert-link" role="alert">
+            <h3 className="alert-heading">
+              {t('admin:maintenance_mode.maintenance_mode')}
+            </h3>
+            <p>
+              {t('admin:maintenance_mode.description')}
+            </p>
+            <hr />
+            <a className="btn-link" href="#maintenance-mode" rel="noopener noreferrer">
+              <i className="fa fa-fw fa-arrow-down ml-1" aria-hidden="true"></i>
+              <strong>{t('admin:maintenance_mode.end_maintenance_mode')}</strong>
+            </a>
+          </div>
+        )
+      }
+      {
+        !isV5Compatible
+          && (
+            <div className="row">
+              <div className="col-lg-12">
+                <h2 className="admin-setting-header">{t('V5 Page Migration')}</h2>
+                <V5PageMigration />
+              </div>
+            </div>
+          )
+      }
+
+      <div className="row">
+        <div className="col-lg-12">
+          <h2 className="admin-setting-header">{t('App Settings')}</h2>
+          <AppSetting />
+        </div>
+      </div>
+
+      <div className="row mt-5">
+        <div className="col-lg-12">
+          <h2 className="admin-setting-header">{t('Site URL settings')}</h2>
+          <SiteUrlSetting />
+        </div>
+      </div>
+
+      <div className="row mt-5">
+        <div className="col-lg-12">
+          <h2 className="admin-setting-header" id="mail-settings">{t('admin:app_setting.mail_settings')}</h2>
+          <MailSetting />
+        </div>
+      </div>
+
+      <div className="row mt-5">
+        <div className="col-lg-12">
+          <h2 className="admin-setting-header">{t('admin:app_setting.file_upload_settings')}</h2>
+          <FileUploadSetting />
+        </div>
+      </div>
+
+      <div className="row mt-5">
+        <div className="col-lg-12">
+          <h2 className="admin-setting-header">{t('admin:app_setting.plugin_settings')}</h2>
+          <PluginSetting />
+        </div>
+      </div>
+
+      <div className="row">
+        <div className="col-lg-12">
+          <h2 className="admin-setting-header" id="maintenance-mode">{t('admin:maintenance_mode.maintenance_mode')}</h2>
+          <MaintenanceMode />
+        </div>
+      </div>
+    </div>
+  );
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const AppSettingsPageContentsWrapper = withUnstatedContainers(AppSettingsPageContents, [AdminAppContainer]);
+
+export default AppSettingsPageContentsWrapper;

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

@@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
 import { useTranslation } from 'react-i18next';
 
 import AdminAppContainer from '~/client/services/AdminAppContainer';
-import AppContainer from '~/client/services/AppContainer';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
@@ -153,11 +152,9 @@ 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,
 };
 

+ 20 - 24
packages/app/src/components/Admin/App/FileUploadSetting.jsx → packages/app/src/components/Admin/App/FileUploadSetting.tsx

@@ -1,26 +1,29 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import React, { useCallback } from 'react';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { useTranslation } from 'react-i18next';
 
-import AppContainer from '~/client/services/AppContainer';
 import AdminAppContainer from '~/client/services/AdminAppContainer';
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 import AwsSetting from './AwsSetting';
 import GcsSettings from './GcsSettings';
 
-function FileUploadSetting(props) {
 
-  const { t, adminAppContainer } = props;
+type Props = {
+  adminAppContainer: AdminAppContainer,
+}
+
+
+const FileUploadSetting = (props: Props) => {
+  const { t } = useTranslation();
+  const { adminAppContainer } = props;
   const { fileUploadType } = adminAppContainer.state;
   const fileUploadTypes = ['aws', 'gcs', 'gridfs', 'local'];
 
-  async function submitHandler() {
-    const { t } = props;
-
+  const submitHandler = useCallback(async() => {
     try {
       await adminAppContainer.updateFileUploadSettingHandler();
       toastSuccess(t('toaster.update_successed', { target: t('admin:app_setting.file_upload_settings') }));
@@ -28,10 +31,10 @@ function FileUploadSetting(props) {
     catch (err) {
       toastError(err);
     }
-  }
+  }, [adminAppContainer, t]);
 
   return (
-    <React.Fragment>
+    <>
       <p className="card well my-3">
         {t('admin:app_setting.file_upload')}
         <br />
@@ -79,21 +82,14 @@ function FileUploadSetting(props) {
       {fileUploadType === 'gcs' && <GcsSettings />}
 
       <AdminUpdateButtonRow onClick={submitHandler} disabled={adminAppContainer.state.retrieveError != null} />
-
-    </React.Fragment>
+    </>
   );
-}
+};
 
 
 /**
  * Wrapper component for using unstated
  */
-const FileUploadSettingWrapper = withUnstatedContainers(FileUploadSetting, [AppContainer, AdminAppContainer]);
-
-FileUploadSetting.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminAppContainer: PropTypes.instanceOf(AdminAppContainer).isRequired,
-};
+const FileUploadSettingWrapper = withUnstatedContainers(FileUploadSetting, [AdminAppContainer]);
 
-export default withTranslation()(FileUploadSettingWrapper);
+export default FileUploadSettingWrapper;

+ 10 - 11
packages/app/src/components/Admin/App/GcsSettings.jsx

@@ -1,16 +1,17 @@
 
 import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
+import PropTypes from 'prop-types';
+import { useTranslation } from 'react-i18next';
 
-import AppContainer from '~/client/services/AppContainer';
 import AdminAppContainer from '~/client/services/AdminAppContainer';
 
+import { withUnstatedContainers } from '../../UnstatedUtils';
+
 
-function GcsSetting(props) {
-  const { t, adminAppContainer } = props;
+const GcsSetting = (props) => {
+  const { t } = useTranslation();
+  const { adminAppContainer } = props;
   const { gcsReferenceFileWithRelayMode, gcsUseOnlyEnvVars } = adminAppContainer.state;
 
   return (
@@ -147,17 +148,15 @@ function GcsSetting(props) {
     </>
   );
 
-}
+};
 
 /**
  * Wrapper component for using unstated
  */
-const GcsSettingWrapper = withUnstatedContainers(GcsSetting, [AppContainer, AdminAppContainer]);
+const GcsSettingWrapper = withUnstatedContainers(GcsSetting, [AdminAppContainer]);
 
 GcsSetting.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminAppContainer: PropTypes.instanceOf(AdminAppContainer).isRequired,
 };
 
-export default withTranslation()(GcsSettingWrapper);
+export default GcsSettingWrapper;

+ 16 - 18
packages/app/src/components/Admin/App/MailSetting.jsx → packages/app/src/components/Admin/App/MailSetting.tsx

@@ -1,24 +1,28 @@
 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 { toastSuccess, toastError } from '~/client/util/apiNotification';
+
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
-import AppContainer from '~/client/services/AppContainer';
-import AdminAppContainer from '~/client/services/AdminAppContainer';
-import SmtpSetting from './SmtpSetting';
 import SesSetting from './SesSetting';
+import SmtpSetting from './SmtpSetting';
+
+
+type Props = {
+  adminAppContainer: AdminAppContainer,
+}
 
 
-function MailSetting(props) {
-  const { t, adminAppContainer } = props;
+const MailSetting = (props: Props) => {
+  const { t } = useTranslation();
+  const { adminAppContainer } = props;
 
   const transmissionMethods = ['smtp', 'ses'];
 
   async function submitHandler() {
-    const { t } = props;
-
     try {
       await adminAppContainer.updateMailSettingHandler();
       toastSuccess(t('toaster.update_successed', { target: t('admin:app_setting.ses_settings') }));
@@ -101,17 +105,11 @@ function MailSetting(props) {
     </React.Fragment>
   );
 
-}
+};
 
 /**
  * Wrapper component for using unstated
  */
-const MailSettingWrapper = withUnstatedContainers(MailSetting, [AppContainer, AdminAppContainer]);
-
-MailSetting.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminAppContainer: PropTypes.instanceOf(AdminAppContainer).isRequired,
-};
+const MailSettingWrapper = withUnstatedContainers(MailSetting, [AdminAppContainer]);
 
-export default withTranslation()(MailSettingWrapper);
+export default MailSettingWrapper;

+ 0 - 79
packages/app/src/components/Admin/App/PluginSetting.jsx

@@ -1,79 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-import loggerFactory from '~/utils/logger';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
-
-import AppContainer from '~/client/services/AppContainer';
-import AdminAppContainer from '~/client/services/AdminAppContainer';
-import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-
-// eslint-disable-next-line no-unused-vars
-const logger = loggerFactory('growi:app:pluginSetting');
-
-class PluginSetting extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.submitHandler = this.submitHandler.bind(this);
-  }
-
-  async submitHandler() {
-    const { t, adminAppContainer } = this.props;
-
-    try {
-      await adminAppContainer.updatePluginSettingHandler();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:app_setting.plugin_settings') }));
-    }
-    catch (err) {
-      toastError(err);
-      logger.error(err);
-    }
-  }
-
-  render() {
-    const { t, adminAppContainer } = this.props;
-
-    return (
-      <React.Fragment>
-        <p className="card well">{t('admin:app_setting.enable_plugin_loading')}</p>
-
-        <div className="row form-group mb-5">
-          <div className="offset-3 col-6 text-left">
-            <div className="custom-control custom-checkbox custom-checkbox-success">
-              <input
-                id="isEnabledPlugins"
-                className="custom-control-input"
-                type="checkbox"
-                checked={adminAppContainer.state.isEnabledPlugins}
-                onChange={(e) => {
-                  adminAppContainer.changeIsEnabledPlugins(e.target.checked);
-                }}
-              />
-              <label className="custom-control-label" htmlFor="isEnabledPlugins">{t('admin:app_setting.load_plugins')}</label>
-            </div>
-          </div>
-        </div>
-
-        <AdminUpdateButtonRow onClick={this.submitHandler} disabled={adminAppContainer.state.retrieveError != null} />
-      </React.Fragment>
-    );
-  }
-
-}
-
-/**
- * Wrapper component for using unstated
- */
-const PluginSettingWrapper = withUnstatedContainers(PluginSetting, [AppContainer, AdminAppContainer]);
-
-PluginSetting.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminAppContainer: PropTypes.instanceOf(AdminAppContainer).isRequired,
-};
-
-export default withTranslation()(PluginSettingWrapper);

+ 66 - 0
packages/app/src/components/Admin/App/PluginSetting.tsx

@@ -0,0 +1,66 @@
+import React, { useCallback } from 'react';
+
+import { useTranslation } from 'react-i18next';
+
+import AdminAppContainer from '~/client/services/AdminAppContainer';
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import loggerFactory from '~/utils/logger';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
+import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
+
+const logger = loggerFactory('growi:app:pluginSetting');
+
+type Props = {
+  adminAppContainer: AdminAppContainer,
+}
+
+const PluginSetting = (props: Props) => {
+  const { t } = useTranslation();
+  const { adminAppContainer } = props;
+
+
+  const submitHandler = useCallback(async() => {
+    try {
+      await adminAppContainer.updatePluginSettingHandler();
+      toastSuccess(t('toaster.update_successed', { target: t('admin:app_setting.plugin_settings') }));
+    }
+    catch (err) {
+      toastError(err);
+      logger.error(err);
+    }
+  }, [adminAppContainer, t]);
+
+  return (
+    <>
+      <p className="card well">{t('admin:app_setting.enable_plugin_loading')}</p>
+
+      <div className="row form-group mb-5">
+        <div className="offset-3 col-6 text-left">
+          <div className="custom-control custom-checkbox custom-checkbox-success">
+            <input
+              id="isEnabledPlugins"
+              className="custom-control-input"
+              type="checkbox"
+              checked={adminAppContainer.state.isEnabledPlugins}
+              onChange={(e) => {
+                adminAppContainer.changeIsEnabledPlugins(e.target.checked);
+              }}
+            />
+            <label className="custom-control-label" htmlFor="isEnabledPlugins">{t('admin:app_setting.load_plugins')}</label>
+          </div>
+        </div>
+      </div>
+
+      <AdminUpdateButtonRow onClick={submitHandler} disabled={adminAppContainer.state.retrieveError != null} />
+    </>
+  );
+
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const PluginSettingWrapper = withUnstatedContainers(PluginSetting, [AdminAppContainer]);
+
+export default PluginSettingWrapper;

+ 10 - 16
packages/app/src/components/Admin/App/SesSetting.jsx → packages/app/src/components/Admin/App/SesSetting.tsx

@@ -1,16 +1,16 @@
 
 import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
-import { withLoadingSppiner } from '../../SuspenseUtils';
+import AdminAppContainer from '~/client/services/AdminAppContainer';
 
+import { withLoadingSppiner } from '../../SuspenseUtils';
+import { withUnstatedContainers } from '../../UnstatedUtils';
 
-import AppContainer from '~/client/services/AppContainer';
-import AdminAppContainer from '~/client/services/AdminAppContainer';
+type Props = {
+  adminAppContainer: AdminAppContainer,
+}
 
-function SmtpSetting(props) {
+const SmtpSetting = (props: Props) => {
   const { adminAppContainer } = props;
 
   return (
@@ -52,17 +52,11 @@ function SmtpSetting(props) {
 
     </React.Fragment>
   );
-}
+};
 
 /**
  * Wrapper component for using unstated
  */
-const SmtpSettingWrapper = withUnstatedContainers(withLoadingSppiner(SmtpSetting), [AppContainer, AdminAppContainer]);
-
-SmtpSetting.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminAppContainer: PropTypes.instanceOf(AdminAppContainer).isRequired,
-};
+const SmtpSettingWrapper = withUnstatedContainers(withLoadingSppiner(SmtpSetting), [AdminAppContainer]);
 
-export default withTranslation()(SmtpSettingWrapper);
+export default SmtpSettingWrapper;

+ 0 - 105
packages/app/src/components/Admin/App/SiteUrlSetting.jsx

@@ -1,105 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-import loggerFactory from '~/utils/logger';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
-
-import AppContainer from '~/client/services/AppContainer';
-import AdminAppContainer from '~/client/services/AdminAppContainer';
-import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-
-const logger = loggerFactory('growi:appSettings');
-
-class SiteUrlSetting extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.submitHandler = this.submitHandler.bind(this);
-  }
-
-  async submitHandler() {
-    const { t, adminAppContainer } = this.props;
-
-    try {
-      await adminAppContainer.updateSiteUrlSettingHandler();
-      toastSuccess(t('toaster.update_successed', { target: t('Site URL settings') }));
-    }
-    catch (err) {
-      toastError(err);
-      logger.error(err);
-    }
-  }
-
-  render() {
-    const { t, adminAppContainer } = this.props;
-
-    return (
-      <React.Fragment>
-        <p className="card well">{t('admin:app_setting.site_url_desc')}</p>
-        {!adminAppContainer.state.isSetSiteUrl
-          && (<p className="alert alert-danger"><i className="icon-exclamation"></i> {t('admin:app_setting.site_url_warn')}</p>)}
-
-        <div className="row form-group">
-          <div className="col-md-9 offset-md-3">
-            <table className="table settings-table">
-              <colgroup>
-                <col className="from-db" />
-                <col className="from-env-vars" />
-              </colgroup>
-              <thead>
-                <tr>
-                  <th>Database</th>
-                  <th>Environment variables</th>
-                </tr>
-              </thead>
-              <tbody>
-                <tr>
-                  <td>
-                    <input
-                      className="form-control"
-                      type="text"
-                      name="settingForm[app:siteUrl]"
-                      defaultValue={adminAppContainer.state.siteUrl || ''}
-                      onChange={(e) => { adminAppContainer.changeSiteUrl(e.target.value) }}
-                      placeholder="e.g. https://my.growi.org"
-                    />
-                    <p className="form-text text-muted">
-                      {/* eslint-disable-next-line react/no-danger */}
-                      <span dangerouslySetInnerHTML={{ __html: t('admin:app_setting.siteurl_help') }} />
-                    </p>
-                  </td>
-                  <td>
-                    <input className="form-control" type="text" value={adminAppContainer.state.envSiteUrl || ''} readOnly />
-                    <p className="form-text text-muted">
-                      {/* eslint-disable-next-line react/no-danger */}
-                      <span dangerouslySetInnerHTML={{ __html: t('admin:app_setting.use_env_var_if_empty', { variable: 'APP_SITE_URL' }) }} />
-                    </p>
-                  </td>
-                </tr>
-              </tbody>
-            </table>
-          </div>
-        </div>
-
-        <AdminUpdateButtonRow onClick={this.submitHandler} disabled={adminAppContainer.state.retrieveError != null} />
-      </React.Fragment>
-    );
-  }
-
-}
-
-/**
- * Wrapper component for using unstated
- */
-const SiteUrlSettingWrapper = withUnstatedContainers(SiteUrlSetting, [AppContainer, AdminAppContainer]);
-
-SiteUrlSetting.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminAppContainer: PropTypes.instanceOf(AdminAppContainer).isRequired,
-};
-
-export default withTranslation()(SiteUrlSettingWrapper);

+ 93 - 0
packages/app/src/components/Admin/App/SiteUrlSetting.tsx

@@ -0,0 +1,93 @@
+import React, { useCallback } from 'react';
+
+import { useTranslation } from 'react-i18next';
+
+import AdminAppContainer from '~/client/services/AdminAppContainer';
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import loggerFactory from '~/utils/logger';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
+import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
+
+const logger = loggerFactory('growi:appSettings');
+
+
+type Props = {
+  adminAppContainer: AdminAppContainer,
+}
+
+const SiteUrlSetting = (props: Props) => {
+  const { t } = useTranslation();
+  const { adminAppContainer } = props;
+
+
+  const submitHandler = useCallback(async() => {
+    try {
+      await adminAppContainer.updateSiteUrlSettingHandler();
+      toastSuccess(t('toaster.update_successed', { target: t('Site URL settings') }));
+    }
+    catch (err) {
+      toastError(err);
+      logger.error(err);
+    }
+  }, [adminAppContainer, t]);
+
+  return (
+    <React.Fragment>
+      <p className="card well">{t('admin:app_setting.site_url_desc')}</p>
+      {!adminAppContainer.state.isSetSiteUrl
+          && (<p className="alert alert-danger"><i className="icon-exclamation"></i> {t('admin:app_setting.site_url_warn')}</p>)}
+
+      <div className="row form-group">
+        <div className="col-md-9 offset-md-3">
+          <table className="table settings-table">
+            <colgroup>
+              <col className="from-db" />
+              <col className="from-env-vars" />
+            </colgroup>
+            <thead>
+              <tr>
+                <th>Database</th>
+                <th>Environment variables</th>
+              </tr>
+            </thead>
+            <tbody>
+              <tr>
+                <td>
+                  <input
+                    className="form-control"
+                    type="text"
+                    name="settingForm[app:siteUrl]"
+                    defaultValue={adminAppContainer.state.siteUrl || ''}
+                    onChange={(e) => { adminAppContainer.changeSiteUrl(e.target.value) }}
+                    placeholder="e.g. https://my.growi.org"
+                  />
+                  <p className="form-text text-muted">
+                    {/* eslint-disable-next-line react/no-danger */}
+                    <span dangerouslySetInnerHTML={{ __html: t('admin:app_setting.siteurl_help') }} />
+                  </p>
+                </td>
+                <td>
+                  <input className="form-control" type="text" value={adminAppContainer.state.envSiteUrl || ''} readOnly />
+                  <p className="form-text text-muted">
+                    {/* eslint-disable-next-line react/no-danger */}
+                    <span dangerouslySetInnerHTML={{ __html: t('admin:app_setting.use_env_var_if_empty', { variable: 'APP_SITE_URL' }) }} />
+                  </p>
+                </td>
+              </tr>
+            </tbody>
+          </table>
+        </div>
+      </div>
+
+      <AdminUpdateButtonRow onClick={submitHandler} disabled={adminAppContainer.state.retrieveError != null} />
+    </React.Fragment>
+  );
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const SiteUrlSettingWrapper = withUnstatedContainers(SiteUrlSetting, [AdminAppContainer]);
+
+export default SiteUrlSettingWrapper;

+ 14 - 17
packages/app/src/components/Admin/App/SmtpSetting.jsx → packages/app/src/components/Admin/App/SmtpSetting.tsx

@@ -1,17 +1,21 @@
 
 import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
+import { useTranslation } from 'react-i18next';
+
+import AdminAppContainer from '~/client/services/AdminAppContainer';
+
 import { withLoadingSppiner } from '../../SuspenseUtils';
+import { withUnstatedContainers } from '../../UnstatedUtils';
 
 
-import AppContainer from '~/client/services/AppContainer';
-import AdminAppContainer from '~/client/services/AdminAppContainer';
+type Props = {
+  adminAppContainer: AdminAppContainer,
+}
 
-function SmtpSetting(props) {
-  const { adminAppContainer, t } = props;
+const SmtpSetting = (props: Props) => {
+  const { t } = useTranslation();
+  const { adminAppContainer } = props;
 
   return (
     <React.Fragment>
@@ -73,17 +77,10 @@ function SmtpSetting(props) {
       </div>
     </React.Fragment>
   );
-}
+};
 
 /**
  * Wrapper component for using unstated
  */
-const SmtpSettingWrapper = withUnstatedContainers(withLoadingSppiner(SmtpSetting), [AppContainer, AdminAppContainer]);
-
-SmtpSetting.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminAppContainer: PropTypes.instanceOf(AdminAppContainer).isRequired,
-};
-
-export default withTranslation()(SmtpSettingWrapper);
+const SmtpSettingWrapper = withUnstatedContainers(withLoadingSppiner(SmtpSetting), [AdminAppContainer]);
+export default SmtpSettingWrapper;

+ 0 - 79
packages/app/src/components/Admin/Customize/CustomizeCssSetting.jsx

@@ -1,79 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-import { Card, CardBody } from 'reactstrap';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
-
-import AppContainer from '~/client/services/AppContainer';
-
-import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
-import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-import CustomCssEditor from '../CustomCssEditor';
-
-class CustomizeCssSetting extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.onClickSubmit = this.onClickSubmit.bind(this);
-  }
-
-  async onClickSubmit() {
-    const { t, adminCustomizeContainer } = this.props;
-
-    try {
-      await adminCustomizeContainer.updateCustomizeCss();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.custom_css') }));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }
-
-  render() {
-    const { t, adminCustomizeContainer } = this.props;
-
-    return (
-      <React.Fragment>
-        <div className="row">
-          <div className="col-12">
-            <h2 className="admin-setting-header">{t('admin:customize_setting.custom_css')}</h2>
-
-            <Card className="card well my-3">
-              <CardBody className="px-0 py-2">
-                { t('admin:customize_setting.write_css') }<br />
-                { t('admin:customize_setting.reflect_change') }
-              </CardBody>
-            </Card>
-
-            <div className="form-group">
-              <CustomCssEditor
-                value={adminCustomizeContainer.state.currentCustomizeCss || ''}
-                onChange={(inputValue) => { adminCustomizeContainer.changeCustomizeCss(inputValue) }}
-              />
-              <p className="form-text text-muted text-right">
-                <i className="fa fa-fw fa-keyboard-o" aria-hidden="true" />
-                {t('admin:customize_setting.ctrl_space')}
-              </p>
-            </div>
-
-            <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
-          </div>
-        </div>
-      </React.Fragment>
-    );
-  }
-
-}
-
-const CustomizeCssSettingWrapper = withUnstatedContainers(CustomizeCssSetting, [AppContainer, AdminCustomizeContainer]);
-
-CustomizeCssSetting.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
-};
-
-export default withTranslation()(CustomizeCssSettingWrapper);

+ 68 - 0
packages/app/src/components/Admin/Customize/CustomizeCssSetting.tsx

@@ -0,0 +1,68 @@
+import React, { useCallback } from 'react';
+
+import { useTranslation } from 'react-i18next';
+import { Card, CardBody } from 'reactstrap';
+
+import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
+import AppContainer from '~/client/services/AppContainer';
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
+import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
+import CustomCssEditor from '../CustomCssEditor';
+
+type Props = {
+  appContainer: AppContainer,
+  adminCustomizeContainer: AdminCustomizeContainer
+}
+
+const CustomizeCssSetting = (props: Props): JSX.Element => {
+
+  const { adminCustomizeContainer } = props;
+  const { t } = useTranslation();
+
+  const onClickSubmit = useCallback(async() => {
+    try {
+      await adminCustomizeContainer.updateCustomizeCss();
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.custom_css') }));
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }, [t, adminCustomizeContainer]);
+
+  return (
+    <React.Fragment>
+      <div className="row">
+        <div className="col-12">
+          <h2 className="admin-setting-header">{t('admin:customize_setting.custom_css')}</h2>
+
+          <Card className="card well my-3">
+            <CardBody className="px-0 py-2">
+              { t('admin:customize_setting.write_css') }<br />
+              { t('admin:customize_setting.reflect_change') }
+            </CardBody>
+          </Card>
+
+          <div className="form-group">
+            <CustomCssEditor
+              value={adminCustomizeContainer.state.currentCustomizeCss || ''}
+              onChange={(inputValue) => { adminCustomizeContainer.changeCustomizeCss(inputValue) }}
+            />
+            <p className="form-text text-muted text-right">
+              <i className="fa fa-fw fa-keyboard-o" aria-hidden="true" />
+              {t('admin:customize_setting.ctrl_space')}
+            </p>
+          </div>
+
+          <AdminUpdateButtonRow onClick={onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
+        </div>
+      </div>
+    </React.Fragment>
+  );
+
+};
+
+const CustomizeCssSettingWrapper = withUnstatedContainers(CustomizeCssSetting, [AppContainer, AdminCustomizeContainer]);
+
+export default CustomizeCssSettingWrapper;

+ 0 - 39
packages/app/src/components/Admin/Customize/CustomizeFunctionOption.jsx

@@ -1,39 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-class CustomizeFunctionOption extends React.PureComponent {
-
-  render() {
-    return (
-      <React.Fragment>
-        <div className="custom-control custom-checkbox custom-checkbox-success">
-          <input
-            className="custom-control-input"
-            type="checkbox"
-            id={this.props.optionId}
-            checked={this.props.isChecked}
-            onChange={this.props.onChecked}
-          />
-          <label className="custom-control-label" htmlFor={this.props.optionId}>
-            <strong>{this.props.label}</strong>
-          </label>
-        </div>
-        {this.props.children}
-      </React.Fragment>
-    );
-  }
-
-}
-
-CustomizeFunctionOption.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-
-  optionId: PropTypes.string.isRequired,
-  label: PropTypes.string.isRequired,
-  isChecked: PropTypes.bool.isRequired,
-  onChecked: PropTypes.func.isRequired,
-  children: PropTypes.object.isRequired,
-};
-
-export default withTranslation()(CustomizeFunctionOption);

+ 37 - 0
packages/app/src/components/Admin/Customize/CustomizeFunctionOption.tsx

@@ -0,0 +1,37 @@
+import React from 'react';
+
+type Props = {
+  optionId: string
+  label: string,
+  isChecked: boolean,
+  onChecked: () => void,
+  children: React.ReactNode,
+}
+
+const CustomizeFunctionOption = (props: Props): JSX.Element => {
+
+  const {
+    optionId, label, isChecked, onChecked, children,
+  } = props;
+
+  return (
+    <React.Fragment>
+      <div className="custom-control custom-checkbox custom-checkbox-success">
+        <input
+          className="custom-control-input"
+          type="checkbox"
+          id={optionId}
+          checked={isChecked}
+          onChange={onChecked}
+        />
+        <label className="custom-control-label" htmlFor={optionId}>
+          <strong>{label}</strong>
+        </label>
+      </div>
+      {children}
+    </React.Fragment>
+  );
+
+};
+
+export default CustomizeFunctionOption;

+ 0 - 174
packages/app/src/components/Admin/Customize/CustomizeFunctionSetting.jsx

@@ -1,174 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-import { Card, CardBody } from 'reactstrap';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
-
-import AppContainer from '~/client/services/AppContainer';
-
-import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
-import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-import CustomizeFunctionOption from './CustomizeFunctionOption';
-import PagingSizeUncontrolledDropdown from './PagingSizeUncontrolledDropdown';
-
-class CustomizeFunctionSetting extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-    };
-    this.onClickSubmit = this.onClickSubmit.bind(this);
-  }
-
-  async onClickSubmit() {
-    const { t, adminCustomizeContainer } = this.props;
-
-    try {
-      await adminCustomizeContainer.updateCustomizeFunction();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.function') }));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }
-
-  render() {
-    const { t, adminCustomizeContainer } = this.props;
-
-    return (
-      <React.Fragment>
-        <div className="row">
-          <div className="col-12">
-            <h2 className="admin-setting-header">{t('admin:customize_setting.function')}</h2>
-            <Card className="card well my-3">
-              <CardBody className="px-0 py-2">
-                {t('admin:customize_setting.function_desc')}
-              </CardBody>
-            </Card>
-
-
-            <div className="form-group row">
-              <div className="offset-md-3 col-md-6 text-left">
-                <CustomizeFunctionOption
-                  optionId="isSavedStatesOfTabChanges"
-                  label={t('admin:customize_setting.function_options.tab_switch')}
-                  isChecked={adminCustomizeContainer.state.isSavedStatesOfTabChanges}
-                  onChecked={() => { adminCustomizeContainer.switchSavedStatesOfTabChanges() }}
-                >
-                  <p className="form-text text-muted">
-                    {t('admin:customize_setting.function_options.tab_switch_desc1')}<br />
-                    {t('admin:customize_setting.function_options.tab_switch_desc2')}
-                  </p>
-                </CustomizeFunctionOption>
-              </div>
-            </div>
-            <div className="form-group row">
-              <div className="offset-md-3 col-md-6 text-left">
-                <CustomizeFunctionOption
-                  optionId="isEnabledAttachTitleHeader"
-                  label={t('admin:customize_setting.function_options.attach_title_header')}
-                  isChecked={adminCustomizeContainer.state.isEnabledAttachTitleHeader}
-                  onChecked={() => { adminCustomizeContainer.switchEnabledAttachTitleHeader() }}
-                >
-                  <p className="form-text text-muted">
-                    {t('admin:customize_setting.function_options.attach_title_header_desc')}
-                  </p>
-                </CustomizeFunctionOption>
-              </div>
-            </div>
-
-            <PagingSizeUncontrolledDropdown
-              label={t('admin:customize_setting.function_options.list_num_s')}
-              desc={t('admin:customize_setting.function_options.list_num_desc_s')}
-              toggleLabel={adminCustomizeContainer.state.pageLimitationS || 20}
-              dropdownItemSize={[10, 20, 50, 100]}
-              onChangeDropdownItem={adminCustomizeContainer.switchPageListLimitationS}
-            />
-            <PagingSizeUncontrolledDropdown
-              label={t('admin:customize_setting.function_options.list_num_m')}
-              desc={t('admin:customize_setting.function_options.list_num_desc_m')}
-              toggleLabel={adminCustomizeContainer.state.pageLimitationM || 10}
-              dropdownItemSize={[5, 10, 20, 50, 100]}
-              onChangeDropdownItem={adminCustomizeContainer.switchPageListLimitationM}
-            />
-            <PagingSizeUncontrolledDropdown
-              label={t('admin:customize_setting.function_options.list_num_l')}
-              desc={t('admin:customize_setting.function_options.list_num_desc_l')}
-              toggleLabel={adminCustomizeContainer.state.pageLimitationL || 50}
-              dropdownItemSize={[20, 50, 100, 200]}
-              onChangeDropdownItem={adminCustomizeContainer.switchPageListLimitationL}
-            />
-            <PagingSizeUncontrolledDropdown
-              label={t('admin:customize_setting.function_options.list_num_xl')}
-              desc={t('admin:customize_setting.function_options.list_num_desc_xl')}
-              toggleLabel={adminCustomizeContainer.state.pageLimitationXL || 20}
-              dropdownItemSize={[5, 10, 20, 50, 100]}
-              onChangeDropdownItem={adminCustomizeContainer.switchPageListLimitationXL}
-            />
-
-            <div className="form-group row">
-              <div className="offset-md-3 col-md-6 text-left">
-                <CustomizeFunctionOption
-                  optionId="isEnabledStaleNotification"
-                  label={t('admin:customize_setting.function_options.stale_notification')}
-                  isChecked={adminCustomizeContainer.state.isEnabledStaleNotification}
-                  onChecked={() => { adminCustomizeContainer.switchEnableStaleNotification() }}
-                >
-                  <p className="form-text text-muted">
-                    {t('admin:customize_setting.function_options.stale_notification_desc')}
-                  </p>
-                </CustomizeFunctionOption>
-              </div>
-            </div>
-
-            <div className="form-group row">
-              <div className="offset-md-3 col-md-6 text-left">
-                <CustomizeFunctionOption
-                  optionId="isAllReplyShown"
-                  label={t('admin:customize_setting.function_options.show_all_reply_comments')}
-                  isChecked={adminCustomizeContainer.state.isAllReplyShown || false}
-                  onChecked={() => { adminCustomizeContainer.switchIsAllReplyShown() }}
-                >
-                  <p className="form-text text-muted">
-                    {t('admin:customize_setting.function_options.show_all_reply_comments_desc')}
-                  </p>
-                </CustomizeFunctionOption>
-              </div>
-            </div>
-
-            <div className="form-group row">
-              <div className="offset-md-3 col-md-6 text-left">
-                <CustomizeFunctionOption
-                  optionId="isSearchScopeChildrenAsDefault"
-                  label={t('admin:customize_setting.function_options.select_search_scope_children_as_default')}
-                  isChecked={adminCustomizeContainer.state.isSearchScopeChildrenAsDefault || false}
-                  onChecked={() => { adminCustomizeContainer.switchIsSearchScopeChildrenAsDefault() }}
-                >
-                  <p className="form-text text-muted">
-                    {t('admin:customize_setting.function_options.select_search_scope_children_as_default_desc')}
-                  </p>
-                </CustomizeFunctionOption>
-              </div>
-            </div>
-
-            <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
-          </div>
-        </div>
-      </React.Fragment>
-    );
-  }
-
-}
-
-const CustomizeFunctionSettingWrapper = withUnstatedContainers(CustomizeFunctionSetting, [AppContainer, AdminCustomizeContainer]);
-
-CustomizeFunctionSetting.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
-};
-
-export default withTranslation()(CustomizeFunctionSettingWrapper);

+ 163 - 0
packages/app/src/components/Admin/Customize/CustomizeFunctionSetting.tsx

@@ -0,0 +1,163 @@
+import React, { useCallback } from 'react';
+
+import { useTranslation } from 'react-i18next';
+import { Card, CardBody } from 'reactstrap';
+
+import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
+import AppContainer from '~/client/services/AppContainer';
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
+import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
+
+import CustomizeFunctionOption from './CustomizeFunctionOption';
+import PagingSizeUncontrolledDropdown from './PagingSizeUncontrolledDropdown';
+
+type Props = {
+  appContainer: AppContainer,
+  adminCustomizeContainer: AdminCustomizeContainer
+}
+
+const CustomizeFunctionSetting = (props: Props): JSX.Element => {
+
+  const { adminCustomizeContainer } = props;
+  const { t } = useTranslation();
+
+  const onClickSubmit = useCallback(async() => {
+
+    try {
+      await adminCustomizeContainer.updateCustomizeFunction();
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.function') }));
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }, [t, adminCustomizeContainer]);
+
+  return (
+    <React.Fragment>
+      <div className="row">
+        <div className="col-12">
+          <h2 className="admin-setting-header">{t('admin:customize_setting.function')}</h2>
+          <Card className="card well my-3">
+            <CardBody className="px-0 py-2">
+              {t('admin:customize_setting.function_desc')}
+            </CardBody>
+          </Card>
+
+
+          <div className="form-group row">
+            <div className="offset-md-3 col-md-6 text-left">
+              <CustomizeFunctionOption
+                optionId="isSavedStatesOfTabChanges"
+                label={t('admin:customize_setting.function_options.tab_switch')}
+                isChecked={adminCustomizeContainer.state.isSavedStatesOfTabChanges}
+                onChecked={() => { adminCustomizeContainer.switchSavedStatesOfTabChanges() }}
+              >
+                <p className="form-text text-muted">
+                  {t('admin:customize_setting.function_options.tab_switch_desc1')}<br />
+                  {t('admin:customize_setting.function_options.tab_switch_desc2')}
+                </p>
+              </CustomizeFunctionOption>
+            </div>
+          </div>
+          <div className="form-group row">
+            <div className="offset-md-3 col-md-6 text-left">
+              <CustomizeFunctionOption
+                optionId="isEnabledAttachTitleHeader"
+                label={t('admin:customize_setting.function_options.attach_title_header')}
+                isChecked={adminCustomizeContainer.state.isEnabledAttachTitleHeader}
+                onChecked={() => { adminCustomizeContainer.switchEnabledAttachTitleHeader() }}
+              >
+                <p className="form-text text-muted">
+                  {t('admin:customize_setting.function_options.attach_title_header_desc')}
+                </p>
+              </CustomizeFunctionOption>
+            </div>
+          </div>
+
+          <PagingSizeUncontrolledDropdown
+            label={t('admin:customize_setting.function_options.list_num_s')}
+            desc={t('admin:customize_setting.function_options.list_num_desc_s')}
+            toggleLabel={adminCustomizeContainer.state.pageLimitationS || 20}
+            dropdownItemSize={[10, 20, 50, 100]}
+            onChangeDropdownItem={adminCustomizeContainer.switchPageListLimitationS}
+          />
+          <PagingSizeUncontrolledDropdown
+            label={t('admin:customize_setting.function_options.list_num_m')}
+            desc={t('admin:customize_setting.function_options.list_num_desc_m')}
+            toggleLabel={adminCustomizeContainer.state.pageLimitationM || 10}
+            dropdownItemSize={[5, 10, 20, 50, 100]}
+            onChangeDropdownItem={adminCustomizeContainer.switchPageListLimitationM}
+          />
+          <PagingSizeUncontrolledDropdown
+            label={t('admin:customize_setting.function_options.list_num_l')}
+            desc={t('admin:customize_setting.function_options.list_num_desc_l')}
+            toggleLabel={adminCustomizeContainer.state.pageLimitationL || 50}
+            dropdownItemSize={[20, 50, 100, 200]}
+            onChangeDropdownItem={adminCustomizeContainer.switchPageListLimitationL}
+          />
+          <PagingSizeUncontrolledDropdown
+            label={t('admin:customize_setting.function_options.list_num_xl')}
+            desc={t('admin:customize_setting.function_options.list_num_desc_xl')}
+            toggleLabel={adminCustomizeContainer.state.pageLimitationXL || 20}
+            dropdownItemSize={[5, 10, 20, 50, 100]}
+            onChangeDropdownItem={adminCustomizeContainer.switchPageListLimitationXL}
+          />
+
+          <div className="form-group row">
+            <div className="offset-md-3 col-md-6 text-left">
+              <CustomizeFunctionOption
+                optionId="isEnabledStaleNotification"
+                label={t('admin:customize_setting.function_options.stale_notification')}
+                isChecked={adminCustomizeContainer.state.isEnabledStaleNotification}
+                onChecked={() => { adminCustomizeContainer.switchEnableStaleNotification() }}
+              >
+                <p className="form-text text-muted">
+                  {t('admin:customize_setting.function_options.stale_notification_desc')}
+                </p>
+              </CustomizeFunctionOption>
+            </div>
+          </div>
+
+          <div className="form-group row">
+            <div className="offset-md-3 col-md-6 text-left">
+              <CustomizeFunctionOption
+                optionId="isAllReplyShown"
+                label={t('admin:customize_setting.function_options.show_all_reply_comments')}
+                isChecked={adminCustomizeContainer.state.isAllReplyShown || false}
+                onChecked={() => { adminCustomizeContainer.switchIsAllReplyShown() }}
+              >
+                <p className="form-text text-muted">
+                  {t('admin:customize_setting.function_options.show_all_reply_comments_desc')}
+                </p>
+              </CustomizeFunctionOption>
+            </div>
+          </div>
+
+          <div className="form-group row">
+            <div className="offset-md-3 col-md-6 text-left">
+              <CustomizeFunctionOption
+                optionId="isSearchScopeChildrenAsDefault"
+                label={t('admin:customize_setting.function_options.select_search_scope_children_as_default')}
+                isChecked={adminCustomizeContainer.state.isSearchScopeChildrenAsDefault || false}
+                onChecked={() => { adminCustomizeContainer.switchIsSearchScopeChildrenAsDefault() }}
+              >
+                <p className="form-text text-muted">
+                  {t('admin:customize_setting.function_options.select_search_scope_children_as_default_desc')}
+                </p>
+              </CustomizeFunctionOption>
+            </div>
+          </div>
+
+          <AdminUpdateButtonRow onClick={onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
+        </div>
+      </div>
+    </React.Fragment>
+  );
+
+};
+
+const CustomizeFunctionSettingWrapper = withUnstatedContainers(CustomizeFunctionSetting, [AppContainer, AdminCustomizeContainer]);
+
+export default CustomizeFunctionSettingWrapper;

+ 0 - 89
packages/app/src/components/Admin/Customize/CustomizeHeaderSetting.jsx

@@ -1,89 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-import { Card, CardBody } from 'reactstrap';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
-
-import AppContainer from '~/client/services/AppContainer';
-
-import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
-import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-import CustomHeaderEditor from '../CustomHeaderEditor';
-
-class CustomizeHeaderSetting extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.onClickSubmit = this.onClickSubmit.bind(this);
-  }
-
-  async onClickSubmit() {
-    const { t, adminCustomizeContainer } = this.props;
-
-    try {
-      await adminCustomizeContainer.updateCustomizeHeader();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.custom_header') }));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }
-
-  render() {
-    const { t, adminCustomizeContainer } = this.props;
-
-    return (
-      <React.Fragment>
-        <div className="row">
-          <div className="col-12">
-            <h2 className="admin-setting-header">{t('admin:customize_setting.custom_header')}</h2>
-
-            <Card className="card well my-3">
-              <CardBody className="px-0 py-2">
-                <span
-                  // eslint-disable-next-line react/no-danger
-                  dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.custom_header_detail') }}
-                />
-              </CardBody>
-            </Card>
-            <div className="form-text text-muted">
-              { t('Example') }:
-              <pre className="hljs">
-                {/* eslint-disable-next-line react/no-unescaped-entities */}
-                <code className="text-wrap">&lt;script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.13.0/build/languages/yaml.min.js"
-                  defer&gt;&lt;/script&gt;
-                </code>
-              </pre>
-            </div>
-
-            <div className="form-group">
-              <CustomHeaderEditor
-                value={adminCustomizeContainer.state.currentCustomizeHeader || ''}
-                onChange={(inputValue) => { adminCustomizeContainer.changeCustomizeHeader(inputValue) }}
-              />
-              <p className="form-text text-muted text-right">
-                <i className="fa fa-fw fa-keyboard-o" aria-hidden="true"></i>
-                {t('admin:customize_setting.ctrl_space')}
-              </p>
-            </div>
-            <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
-          </div>
-        </div>
-      </React.Fragment>
-    );
-  }
-
-}
-
-const CustomizeHeaderSettingWrapper = withUnstatedContainers(CustomizeHeaderSetting, [AppContainer, AdminCustomizeContainer]);
-
-CustomizeHeaderSetting.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
-};
-
-export default withTranslation()(CustomizeHeaderSettingWrapper);

+ 76 - 0
packages/app/src/components/Admin/Customize/CustomizeHeaderSetting.tsx

@@ -0,0 +1,76 @@
+import React, { useCallback } from 'react';
+
+import { useTranslation } from 'react-i18next';
+import { Card, CardBody } from 'reactstrap';
+
+import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
+import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
+import CustomHeaderEditor from '../CustomHeaderEditor';
+
+type Props = {
+  adminCustomizeContainer: AdminCustomizeContainer
+}
+
+const CustomizeHeaderSetting = (props: Props): JSX.Element => {
+
+  const { adminCustomizeContainer } = props;
+  const { t } = useTranslation();
+
+  const onClickSubmit = useCallback(async() => {
+    try {
+      await adminCustomizeContainer.updateCustomizeHeader();
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.custom_header') }));
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }, [t, adminCustomizeContainer]);
+
+  return (
+    <React.Fragment>
+      <div className="row">
+        <div className="col-12">
+          <h2 className="admin-setting-header">{t('admin:customize_setting.custom_header')}</h2>
+
+          <Card className="card well my-3">
+            <CardBody className="px-0 py-2">
+              <span
+                // eslint-disable-next-line react/no-danger
+                dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.custom_header_detail') }}
+              />
+            </CardBody>
+          </Card>
+          <div className="form-text text-muted">
+            { t('Example') }:
+            <pre className="hljs">
+              {/* eslint-disable-next-line react/no-unescaped-entities */}
+              <code className="text-wrap">&lt;script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.13.0/build/languages/yaml.min.js"
+                defer&gt;&lt;/script&gt;
+              </code>
+            </pre>
+          </div>
+
+          <div className="form-group">
+            <CustomHeaderEditor
+              value={adminCustomizeContainer.state.currentCustomizeHeader || ''}
+              onChange={(inputValue) => { adminCustomizeContainer.changeCustomizeHeader(inputValue) }}
+            />
+            <p className="form-text text-muted text-right">
+              <i className="fa fa-fw fa-keyboard-o" aria-hidden="true"></i>
+              {t('admin:customize_setting.ctrl_space')}
+            </p>
+          </div>
+          <AdminUpdateButtonRow onClick={onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
+        </div>
+      </div>
+    </React.Fragment>
+  );
+
+};
+
+const CustomizeHeaderSettingWrapper = withUnstatedContainers(CustomizeHeaderSetting, [AdminCustomizeContainer]);
+
+export default CustomizeHeaderSettingWrapper;

+ 0 - 156
packages/app/src/components/Admin/Customize/CustomizeHighlightSetting.jsx

@@ -1,156 +0,0 @@
-/* eslint-disable no-useless-escape */
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-import {
-  Dropdown, DropdownToggle, DropdownMenu, DropdownItem,
-} from 'reactstrap';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
-
-import AppContainer from '~/client/services/AppContainer';
-
-import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
-import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-
-class CustomizeHighlightSetting extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      isDropdownOpen: false,
-    };
-
-    this.onToggleDropdown = this.onToggleDropdown.bind(this);
-    this.onClickSubmit = this.onClickSubmit.bind(this);
-  }
-
-  onToggleDropdown() {
-    this.setState({ isDropdownOpen: !this.state.isDropdownOpen });
-  }
-
-  async onClickSubmit() {
-    const { t, adminCustomizeContainer } = this.props;
-
-    try {
-      await adminCustomizeContainer.updateHighlightJsStyle();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.code_highlight') }));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }
-
-  renderHljsDemo() {
-    const { adminCustomizeContainer } = this.props;
-
-    /* eslint-disable max-len */
-    const html = `<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">MersenneTwister</span>(<span class="hljs-params">seed</span>) </span>{
-  <span class="hljs-keyword">if</span> (<span class="hljs-built_in">arguments</span>.length == <span class="hljs-number">0</span>) {
-    seed = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().getTime();
-  }
-
-  <span class="hljs-keyword">this</span>._mt = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Array</span>(<span class="hljs-number">624</span>);
-  <span class="hljs-keyword">this</span>.setSeed(seed);
-}</span>`;
-    /* eslint-enable max-len */
-
-    return (
-      <pre className={`hljs ${!adminCustomizeContainer.state.isHighlightJsStyleBorderEnabled && 'hljs-no-border'}`}>
-        {/* eslint-disable-next-line react/no-danger */}
-        <code dangerouslySetInnerHTML={{ __html: html }}></code>
-      </pre>
-    );
-  }
-
-  render() {
-    const { t, adminCustomizeContainer } = this.props;
-    const options = adminCustomizeContainer.state.highlightJsCssSelectorOptions;
-    const menuItem = [];
-
-    Object.entries(options).forEach((option) => {
-      const styleId = option[0];
-      const styleName = option[1].name;
-      const isBorderEnable = option[1].border;
-
-      menuItem.push(
-        <DropdownItem
-          key={styleId}
-          role="presentation"
-          onClick={() => adminCustomizeContainer.switchHighlightJsStyle(styleId, styleName, isBorderEnable)}
-        >
-          <a role="menuitem">{styleName}</a>
-        </DropdownItem>,
-      );
-    });
-
-    return (
-      <React.Fragment>
-        <div className="row">
-          <div className="col-12">
-            <h2 className="admin-setting-header">{t('admin:customize_setting.code_highlight')}</h2>
-
-            <div className="form-group row">
-              <div className="offset-md-3 col-md-6 text-left">
-                <div className="my-0">
-                  <label>{t('admin:customize_setting.theme')}</label>
-                </div>
-                <Dropdown isOpen={this.state.isDropdownOpen} toggle={this.onToggleDropdown}>
-                  <DropdownToggle className="text-right col-6" caret>
-                    <span className="float-left">{adminCustomizeContainer.state.currentHighlightJsStyleName}</span>
-                  </DropdownToggle>
-                  <DropdownMenu className="dropdown-menu" role="menu">
-                    {menuItem}
-                  </DropdownMenu>
-                </Dropdown>
-                <p className="form-text text-warning">
-                  {/* eslint-disable-next-line react/no-danger */}
-                  <span dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.nocdn_desc') }} />
-                </p>
-              </div>
-            </div>
-
-            <div className="form-group row">
-              <div className="offset-md-3 col-md-6 text-left">
-                <div className="custom-control custom-switch custom-checkbox-success">
-                  <input
-                    type="checkbox"
-                    className="custom-control-input"
-                    id="highlightBorder"
-                    checked={adminCustomizeContainer.state.isHighlightJsStyleBorderEnabled}
-                    onChange={() => { adminCustomizeContainer.switchHighlightJsStyleBorder() }}
-                  />
-                  <label className="custom-control-label" htmlFor="highlightBorder">
-                    <strong>Border</strong>
-                  </label>
-                </div>
-              </div>
-            </div>
-
-            <div className="form-text text-muted">
-              <label>Examples:</label>
-              <div className="wiki">
-                {this.renderHljsDemo()}
-              </div>
-            </div>
-
-            <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
-          </div>
-        </div>
-      </React.Fragment>
-    );
-  }
-
-}
-
-const CustomizeHighlightSettingWrapper = withUnstatedContainers(CustomizeHighlightSetting, [AppContainer, AdminCustomizeContainer]);
-
-CustomizeHighlightSetting.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
-};
-
-export default withTranslation()(CustomizeHighlightSettingWrapper);

+ 145 - 0
packages/app/src/components/Admin/Customize/CustomizeHighlightSetting.tsx

@@ -0,0 +1,145 @@
+/* eslint-disable no-useless-escape */
+import React, { useCallback, useState } from 'react';
+
+import { useTranslation } from 'react-i18next';
+import {
+  Dropdown, DropdownToggle, DropdownMenu, DropdownItem,
+} from 'reactstrap';
+
+import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
+import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
+
+type Props = {
+  adminCustomizeContainer: AdminCustomizeContainer
+}
+
+type HljsDemoProps = {
+  isHighlightJsStyleBorderEnabled: boolean
+}
+
+const HljsDemo = React.memo((props: HljsDemoProps): JSX.Element => {
+
+  const { isHighlightJsStyleBorderEnabled } = props;
+
+  /* eslint-disable max-len */
+  const html = `<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">MersenneTwister</span>(<span class="hljs-params">seed</span>) </span>{
+<span class="hljs-keyword">if</span> (<span class="hljs-built_in">arguments</span>.length == <span class="hljs-number">0</span>) {
+  seed = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().getTime();
+}
+
+<span class="hljs-keyword">this</span>._mt = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Array</span>(<span class="hljs-number">624</span>);
+<span class="hljs-keyword">this</span>.setSeed(seed);
+}</span>`;
+  /* eslint-enable max-len */
+
+  return (
+    <pre className={`hljs ${!isHighlightJsStyleBorderEnabled && 'hljs-no-border'}`}>
+      {/* eslint-disable-next-line react/no-danger */}
+      <code dangerouslySetInnerHTML={{ __html: html }}></code>
+    </pre>
+  );
+});
+
+const CustomizeHighlightSetting = (props: Props): JSX.Element => {
+  const { adminCustomizeContainer } = props;
+  const { t } = useTranslation();
+  const [isDropdownOpen, setIsDropdownOpen] = useState(false);
+  const options = adminCustomizeContainer.state.highlightJsCssSelectorOptions;
+
+  const onToggleDropdown = useCallback(() => {
+    setIsDropdownOpen(!isDropdownOpen);
+  }, [isDropdownOpen]);
+
+  const onClickSubmit = useCallback(async() => {
+    try {
+      await adminCustomizeContainer.updateHighlightJsStyle();
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.code_highlight') }));
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }, [t, adminCustomizeContainer]);
+
+  const renderMenuItems = useCallback(() => {
+
+    const items = Object.entries(options).map((option) => {
+      const styleId = option[0];
+      const styleName = option[1].name;
+      const isBorderEnable = option[1].border;
+
+      return (
+        <DropdownItem
+          key={styleId}
+          role="presentation"
+          onClick={() => adminCustomizeContainer.switchHighlightJsStyle(styleId, styleName, isBorderEnable)}
+        >
+          <a role="menuitem">{styleName}</a>
+        </DropdownItem>
+      );
+    });
+    return items;
+  }, [adminCustomizeContainer, options]);
+
+  return (
+    <React.Fragment>
+      <div className="row">
+        <div className="col-12">
+          <h2 className="admin-setting-header">{t('admin:customize_setting.code_highlight')}</h2>
+
+          <div className="form-group row">
+            <div className="offset-md-3 col-md-6 text-left">
+              <div className="my-0">
+                <label>{t('admin:customize_setting.theme')}</label>
+              </div>
+              <Dropdown isOpen={isDropdownOpen} toggle={onToggleDropdown}>
+                <DropdownToggle className="text-right col-6" caret>
+                  <span className="float-left">{adminCustomizeContainer.state.currentHighlightJsStyleName}</span>
+                </DropdownToggle>
+                <DropdownMenu className="dropdown-menu" role="menu">
+                  {renderMenuItems()}
+                </DropdownMenu>
+              </Dropdown>
+              <p className="form-text text-warning">
+                {/* eslint-disable-next-line react/no-danger */}
+                <span dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.nocdn_desc') }} />
+              </p>
+            </div>
+          </div>
+
+          <div className="form-group row">
+            <div className="offset-md-3 col-md-6 text-left">
+              <div className="custom-control custom-switch custom-checkbox-success">
+                <input
+                  type="checkbox"
+                  className="custom-control-input"
+                  id="highlightBorder"
+                  checked={adminCustomizeContainer.state.isHighlightJsStyleBorderEnabled}
+                  onChange={() => { adminCustomizeContainer.switchHighlightJsStyleBorder() }}
+                />
+                <label className="custom-control-label" htmlFor="highlightBorder">
+                  <strong>Border</strong>
+                </label>
+              </div>
+            </div>
+          </div>
+
+          <div className="form-text text-muted">
+            <label>Examples:</label>
+            <div className="wiki">
+              <HljsDemo isHighlightJsStyleBorderEnabled={adminCustomizeContainer.state.isHighlightJsStyleBorderEnabled} />
+            </div>
+          </div>
+
+          <AdminUpdateButtonRow onClick={onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
+        </div>
+      </div>
+    </React.Fragment>
+  );
+};
+
+const CustomizeHighlightSettingWrapper = withUnstatedContainers(CustomizeHighlightSetting, [AdminCustomizeContainer]);
+
+export default CustomizeHighlightSettingWrapper;

+ 4 - 12
packages/app/src/components/Admin/Customize/CustomizeLayoutSetting.jsx → packages/app/src/components/Admin/Customize/CustomizeLayoutSetting.tsx

@@ -1,9 +1,7 @@
 import React, { useCallback, useEffect, 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 { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
 import { isDarkMode as isDarkModeByUtil } from '~/client/util/color-scheme';
@@ -11,8 +9,8 @@ import { isDarkMode as isDarkModeByUtil } from '~/client/util/color-scheme';
 const isDarkMode = isDarkModeByUtil();
 const colorText = isDarkMode ? 'dark' : 'light';
 
-const CustomizeLayoutSetting = (props) => {
-  const { t, appContainer } = props;
+const CustomizeLayoutSetting = (): JSX.Element => {
+  const { t } = useTranslation();
 
   const [isContainerFluid, setIsContainerFluid] = useState(false);
   const [retrieveError, setRetrieveError] = useState();
@@ -85,10 +83,4 @@ const CustomizeLayoutSetting = (props) => {
   );
 };
 
-CustomizeLayoutSetting.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-};
-
-export default withTranslation()(CustomizeLayoutSetting);
+export default CustomizeLayoutSetting;

+ 0 - 120
packages/app/src/components/Admin/Customize/CustomizeScriptSetting.jsx

@@ -1,120 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-import { Card, CardBody } from 'reactstrap';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
-
-import AppContainer from '~/client/services/AppContainer';
-
-import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
-import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-import CustomScriptEditor from '../CustomScriptEditor';
-
-class CustomizeScriptSetting extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.onClickSubmit = this.onClickSubmit.bind(this);
-  }
-
-  async onClickSubmit() {
-    const { t, adminCustomizeContainer } = this.props;
-
-    try {
-      await adminCustomizeContainer.updateCustomizeScript();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.custom_script') }));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }
-
-  getExampleCode() {
-    return `console.log($('.main-container'));
-    window.addEventListener('load', (event) => {
-      console.log('config: ', appContainer.config);
-    });
-    `;
-  }
-
-  render() {
-    const { t, adminCustomizeContainer } = this.props;
-
-    return (
-      <React.Fragment>
-        <div className="row">
-          <div className="col-12">
-            <h2 className="admin-setting-header">{t('admin:customize_setting.custom_script')}</h2>
-            <Card className="card well">
-              <CardBody className="px-0 py-2">
-                {t('admin:customize_setting.write_java')}<br />
-                {t('admin:customize_setting.reflect_change')}
-              </CardBody>
-            </Card>
-
-            <div className="form-text text-muted">
-              Placeholders:<br />
-              (Available after <code>load</code> event)
-            </div>
-            <table className="table table-borderless table-sm form-text text-muted offset-1 col-11">
-              <tbody>
-                <tr>
-                  <th className="text-right"><code>$</code></th>
-                  <td>jQuery instance</td>
-                </tr>
-                <tr>
-                  <th className="text-right"><code>appContainer</code></th>
-                  <td>GROWI App <a href="https://github.com/jamiebuilds/unstated">unstated container</a></td>
-                </tr>
-                <tr>
-                  <th className="text-right"><code>growiRenderer</code></th>
-                  <td>GROWI Renderer origin instance</td>
-                </tr>
-                <tr>
-                  <th className="text-right"><code>growiPlugin</code></th>
-                  <td>GROWI Plugin Manager instance</td>
-                </tr>
-                <tr>
-                  <th className="text-right"><code>Crowi</code></th>
-                  <td>Crowi legacy instance (jQuery based)</td>
-                </tr>
-              </tbody>
-            </table>
-
-            <div className="form-text text-muted">
-              Examples:
-              <pre className="hljs"><code>{this.getExampleCode()}</code></pre>
-            </div>
-
-            <div className="form-group">
-              <CustomScriptEditor
-                value={adminCustomizeContainer.state.currentCustomizeScript || ''}
-                onChange={(inputValue) => { adminCustomizeContainer.changeCustomizeScript(inputValue) }}
-              />
-              <p className="form-text text-muted text-right">
-                <i className="fa fa-fw fa-keyboard-o" aria-hidden="true" />
-                {t('admin:customize_setting.ctrl_space')}
-              </p>
-            </div>
-
-            <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
-          </div>
-        </div>
-      </React.Fragment>
-    );
-  }
-
-}
-
-const CustomizeScriptSettingWrapper = withUnstatedContainers(CustomizeScriptSetting, [AppContainer, AdminCustomizeContainer]);
-
-CustomizeScriptSetting.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
-};
-
-export default withTranslation()(CustomizeScriptSettingWrapper);

+ 107 - 0
packages/app/src/components/Admin/Customize/CustomizeScriptSetting.tsx

@@ -0,0 +1,107 @@
+import React, { useCallback } from 'react';
+
+import { useTranslation } from 'react-i18next';
+import { Card, CardBody } from 'reactstrap';
+
+import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
+import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
+import CustomScriptEditor from '../CustomScriptEditor';
+
+type Props = {
+  adminCustomizeContainer: AdminCustomizeContainer
+}
+
+const CustomizeScriptSetting = (props: Props): JSX.Element => {
+
+  const { adminCustomizeContainer } = props;
+  const { t } = useTranslation();
+
+  const onClickSubmit = useCallback(async() => {
+    try {
+      await adminCustomizeContainer.updateCustomizeScript();
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.custom_script') }));
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }, [t, adminCustomizeContainer]);
+
+  const getExampleCode = useCallback(() => {
+    return `console.log($('.main-container'));
+    window.addEventListener('load', (event) => {
+      console.log('config: ', appContainer.config);
+    });
+    `;
+  }, []);
+
+  return (
+    <React.Fragment>
+      <div className="row">
+        <div className="col-12">
+          <h2 className="admin-setting-header">{t('admin:customize_setting.custom_script')}</h2>
+          <Card className="card well">
+            <CardBody className="px-0 py-2">
+              {t('admin:customize_setting.write_java')}<br />
+              {t('admin:customize_setting.reflect_change')}
+            </CardBody>
+          </Card>
+
+          <div className="form-text text-muted">
+            Placeholders:<br />
+            (Available after <code>load</code> event)
+          </div>
+          <table className="table table-borderless table-sm form-text text-muted offset-1 col-11">
+            <tbody>
+              <tr>
+                <th className="text-right"><code>$</code></th>
+                <td>jQuery instance</td>
+              </tr>
+              <tr>
+                <th className="text-right"><code>appContainer</code></th>
+                <td>GROWI App <a href="https://github.com/jamiebuilds/unstated">unstated container</a></td>
+              </tr>
+              <tr>
+                <th className="text-right"><code>growiRenderer</code></th>
+                <td>GROWI Renderer origin instance</td>
+              </tr>
+              <tr>
+                <th className="text-right"><code>growiPlugin</code></th>
+                <td>GROWI Plugin Manager instance</td>
+              </tr>
+              <tr>
+                <th className="text-right"><code>Crowi</code></th>
+                <td>Crowi legacy instance (jQuery based)</td>
+              </tr>
+            </tbody>
+          </table>
+
+          <div className="form-text text-muted">
+            Examples:
+            <pre className="hljs"><code>{getExampleCode()}</code></pre>
+          </div>
+
+          <div className="form-group">
+            <CustomScriptEditor
+              value={adminCustomizeContainer.state.currentCustomizeScript || ''}
+              onChange={(inputValue) => { adminCustomizeContainer.changeCustomizeScript(inputValue) }}
+            />
+            <p className="form-text text-muted text-right">
+              <i className="fa fa-fw fa-keyboard-o" aria-hidden="true" />
+              {t('admin:customize_setting.ctrl_space')}
+            </p>
+          </div>
+
+          <AdminUpdateButtonRow onClick={onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
+        </div>
+      </div>
+    </React.Fragment>
+  );
+
+};
+
+const CustomizeScriptSettingWrapper = withUnstatedContainers(CustomizeScriptSetting, [AdminCustomizeContainer]);
+
+export default CustomizeScriptSettingWrapper;

+ 78 - 80
packages/app/src/components/Admin/Customize/CustomizeThemeOptions.jsx

@@ -1,102 +1,100 @@
 import React from 'react';
+
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
+
 
+import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
-import AppContainer from '~/client/services/AppContainer';
 import ThemeColorBox from './ThemeColorBox';
-import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
 
-class CustomizeThemeOptions extends React.Component {
+/* eslint-disable no-multi-spaces */
+const lightNDarkTheme = [{
+  name: 'default',    bg: '#ffffff', topbar: '#2a2929', sidebar: '#122c55', theme: '#209fd8',
+}, {
+  name: 'mono-blue',  bg: '#F7FBFD', topbar: '#2a2929', sidebar: '#00587A', theme: '#00587A',
+}, {
+  name: 'hufflepuff',  bg: '#EFE2CF', topbar: '#2a2929', sidebar: '#EAAB20', theme: '#993439',
+}, {
+  name: 'fire-red',  bg: '#FDFDFD', topbar: '#2c2c2c', sidebar: '#BFBFBF', theme: '#EA5532',
+}, {
+  name: 'jade-green',  bg: '#FDFDFD', topbar: '#2c2c2c', sidebar: '#BFBFBF', theme: '#38B48B',
+}];
 
-  render() {
-    const { t, adminCustomizeContainer } = this.props;
-    const { currentLayout, currentTheme } = adminCustomizeContainer.state;
+const uniqueTheme = [{
+  name: 'nature',     bg: '#f9fff3', topbar: '#234136', sidebar: '#118050', theme: '#460039',
+}, {
+  name: 'wood',       bg: '#fffefb', topbar: '#2a2929', sidebar: '#aaa45f', theme: '#aaa45f',
+}, {
+  name: 'island',     bg: '#cef2ef', topbar: '#2a2929', sidebar: '#0c2a44', theme: 'rgba(183, 226, 219, 1)',
+}, {
+  name: 'christmas',  bg: '#fffefb', topbar: '#b3000c', sidebar: '#30882c', theme: '#d3c665',
+}, {
+  name: 'antarctic',  bg: '#ffffff', topbar: '#2a2929', sidebar: '#000080', theme: '#fa9913',
+}, {
+  name: 'spring',     bg: '#ffffff', topbar: '#d3687c', sidebar: '#ffb8c6', theme: '#67a856',
+}, {
+  name: 'future',     bg: '#16282d', topbar: '#2a2929', sidebar: '#00b5b7', theme: '#00b5b7',
+}, {
+  name: 'halloween',  bg: '#030003', topbar: '#aa4a04', sidebar: '#162b33', theme: '#e9af2b',
+}, {
+  name: 'kibela',  bg: '#f4f5f6', topbar: '#1256a3', sidebar: '#5882fa', theme: '#b5cbf79c',
+}, {
+  name: 'blackboard',  bg: '#223729', topbar: '#563E23', sidebar: '#7B5932', theme: '#DA8506',
+}];
 
-    /* eslint-disable no-multi-spaces */
-    const lightNDarkTheme = [{
-      name: 'default',    bg: '#ffffff', topbar: '#2a2929', sidebar: '#122c55', theme: '#209fd8',
-    }, {
-      name: 'mono-blue',  bg: '#F7FBFD', topbar: '#2a2929', sidebar: '#00587A', theme: '#00587A',
-    }, {
-      name: 'hufflepuff',  bg: '#EFE2CF', topbar: '#2a2929', sidebar: '#EAAB20', theme: '#993439',
-    }, {
-      name: 'fire-red',  bg: '#FDFDFD', topbar: '#2c2c2c', sidebar: '#BFBFBF', theme: '#EA5532',
-    }, {
-      name: 'jade-green',  bg: '#FDFDFD', topbar: '#2c2c2c', sidebar: '#BFBFBF', theme: '#38B48B',
-    }];
 
-    const uniqueTheme = [{
-      name: 'nature',     bg: '#f9fff3', topbar: '#234136', sidebar: '#118050', theme: '#460039',
-    }, {
-      name: 'wood',       bg: '#fffefb', topbar: '#2a2929', sidebar: '#aaa45f', theme: '#aaa45f',
-    }, {
-      name: 'island',     bg: '#cef2ef', topbar: '#2a2929', sidebar: '#0c2a44', theme: 'rgba(183, 226, 219, 1)',
-    }, {
-      name: 'christmas',  bg: '#fffefb', topbar: '#b3000c', sidebar: '#30882c', theme: '#d3c665',
-    }, {
-      name: 'antarctic',  bg: '#ffffff', topbar: '#2a2929', sidebar: '#000080', theme: '#fa9913',
-    }, {
-      name: 'spring',     bg: '#ffffff', topbar: '#d3687c', sidebar: '#ffb8c6', theme: '#67a856',
-    }, {
-      name: 'future',     bg: '#16282d', topbar: '#2a2929', sidebar: '#00b5b7', theme: '#00b5b7',
-    }, {
-      name: 'halloween',  bg: '#030003', topbar: '#aa4a04', sidebar: '#162b33', theme: '#e9af2b',
-    }, {
-      name: 'kibela',  bg: '#f4f5f6', topbar: '#1256a3', sidebar: '#5882fa', theme: '#b5cbf79c',
-    }, {
-      name: 'blackboard',  bg: '#223729', topbar: '#563E23', sidebar: '#7B5932', theme: '#DA8506',
-    }];
-    /* eslint-enable no-multi-spaces */
+const CustomizeThemeOptions = (props) => {
 
-    return (
-      <div id="themeOptions" className={`${currentLayout === 'kibela' && 'disabled'}`}>
-        {/* Light and Dark Themes */}
-        <div>
-          <h3>{t('admin:customize_setting.theme_desc.light_and_dark')}</h3>
-          <div className="d-flex flex-wrap">
-            {lightNDarkTheme.map((theme) => {
-              return (
-                <ThemeColorBox
-                  key={theme.name}
-                  isSelected={currentTheme === theme.name}
-                  onSelected={() => adminCustomizeContainer.switchThemeType(theme.name)}
-                  {...theme}
-                />
-              );
-            })}
-          </div>
+  const { adminCustomizeContainer } = props;
+  const { t } = useTranslation();
+  const { currentLayout, currentTheme } = adminCustomizeContainer.state;
+
+  return (
+    <div id="themeOptions" className={`${currentLayout === 'kibela' && 'disabled'}`}>
+      {/* Light and Dark Themes */}
+      <div>
+        <h3>{t('admin:customize_setting.theme_desc.light_and_dark')}</h3>
+        <div className="d-flex flex-wrap">
+          {lightNDarkTheme.map((theme) => {
+            return (
+              <ThemeColorBox
+                key={theme.name}
+                isSelected={currentTheme === theme.name}
+                onSelected={() => adminCustomizeContainer.switchThemeType(theme.name)}
+                {...theme}
+              />
+            );
+          })}
         </div>
-        {/* Unique Theme */}
-        <div className="mt-3">
-          <h3>{t('admin:customize_setting.theme_desc.unique')}</h3>
-          <div className="d-flex flex-wrap">
-            {uniqueTheme.map((theme) => {
-              return (
-                <ThemeColorBox
-                  key={theme.name}
-                  isSelected={currentTheme === theme.name}
-                  onSelected={() => adminCustomizeContainer.switchThemeType(theme.name)}
-                  {...theme}
-                />
-              );
-            })}
-          </div>
+      </div>
+      {/* Unique Theme */}
+      <div className="mt-3">
+        <h3>{t('admin:customize_setting.theme_desc.unique')}</h3>
+        <div className="d-flex flex-wrap">
+          {uniqueTheme.map((theme) => {
+            return (
+              <ThemeColorBox
+                key={theme.name}
+                isSelected={currentTheme === theme.name}
+                onSelected={() => adminCustomizeContainer.switchThemeType(theme.name)}
+                {...theme}
+              />
+            );
+          })}
         </div>
       </div>
-    );
-  }
+    </div>
+  );
 
-}
+};
 
-const CustomizeThemeOptionsWrapper = withUnstatedContainers(CustomizeThemeOptions, [AppContainer, AdminCustomizeContainer]);
+const CustomizeThemeOptionsWrapper = withUnstatedContainers(CustomizeThemeOptions, [AdminCustomizeContainer]);
 
 CustomizeThemeOptions.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
 };
 
-export default withTranslation()(CustomizeThemeOptionsWrapper);
+export default CustomizeThemeOptionsWrapper;

+ 0 - 72
packages/app/src/components/Admin/Customize/CustomizeThemeSetting.jsx

@@ -1,72 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
-
-import AppContainer from '~/client/services/AppContainer';
-
-import CustomizeThemeOptions from './CustomizeThemeOptions';
-import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
-import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-
-class CustomizeThemeSetting extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.onClickSubmit = this.onClickSubmit.bind(this);
-  }
-
-  async onClickSubmit() {
-    const { t, adminCustomizeContainer } = this.props;
-
-    try {
-      await adminCustomizeContainer.updateCustomizeTheme();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.theme') }));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }
-
-  renderDevAlert() {
-    if (process.env.NODE_ENV === 'development') {
-      return (
-        <div className="alert alert-warning">
-          <strong>DEBUG MESSAGE:</strong> Live preview for theme is disabled in development mode.
-        </div>
-      );
-    }
-  }
-
-
-  render() {
-    const { t, adminCustomizeContainer } = this.props;
-
-    return (
-      <React.Fragment>
-        <div className="row">
-          <div className="col-12">
-            <h2 className="admin-setting-header">{t('admin:customize_setting.theme')}</h2>
-            {this.renderDevAlert()}
-            <CustomizeThemeOptions />
-            <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
-          </div>
-        </div>
-      </React.Fragment>
-    );
-  }
-
-}
-
-const CustomizeThemeSettingWrapper = withUnstatedContainers(CustomizeThemeSetting, [AppContainer, AdminCustomizeContainer]);
-
-CustomizeThemeSetting.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
-};
-
-export default withTranslation()(CustomizeThemeSettingWrapper);

+ 58 - 0
packages/app/src/components/Admin/Customize/CustomizeThemeSetting.tsx

@@ -0,0 +1,58 @@
+import React, { useCallback } from 'react';
+
+import { useTranslation } from 'react-i18next';
+
+import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
+import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
+
+import CustomizeThemeOptions from './CustomizeThemeOptions';
+
+type Props = {
+  adminCustomizeContainer: AdminCustomizeContainer
+}
+
+const CustomizeThemeSetting = (props: Props): JSX.Element => {
+
+  const { adminCustomizeContainer } = props;
+  const { t } = useTranslation();
+
+  const onClickSubmit = useCallback(async() => {
+    try {
+      await adminCustomizeContainer.updateCustomizeTheme();
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.theme') }));
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }, [t, adminCustomizeContainer]);
+
+  const renderDevAlert = useCallback(() => {
+    if (process.env.NODE_ENV === 'development') {
+      return (
+        <div className="alert alert-warning">
+          <strong>DEBUG MESSAGE:</strong> Live preview for theme is disabled in development mode.
+        </div>
+      );
+    }
+  }, []);
+
+  return (
+    <React.Fragment>
+      <div className="row">
+        <div className="col-12">
+          <h2 className="admin-setting-header">{t('admin:customize_setting.theme')}</h2>
+          {renderDevAlert()}
+          <CustomizeThemeOptions />
+          <AdminUpdateButtonRow onClick={onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
+        </div>
+      </div>
+    </React.Fragment>
+  );
+};
+
+const CustomizeThemeSettingWrapper = withUnstatedContainers(CustomizeThemeSetting, [AdminCustomizeContainer]);
+
+export default CustomizeThemeSettingWrapper;

+ 2 - 3
packages/app/src/components/Admin/Customize/PagingSizeUncontrolledDropdown.jsx

@@ -1,6 +1,6 @@
 import React from 'react';
+
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
 import {
   UncontrolledDropdown, DropdownToggle, DropdownMenu, DropdownItem,
 } from 'reactstrap';
@@ -47,7 +47,6 @@ const PagingSizeUncontrolledDropdown = (props) => {
 
 
 PagingSizeUncontrolledDropdown.propTypes = {
-  t: PropTypes.func.isRequired, //  i18next
   label: PropTypes.string,
   toggleLabel: PropTypes.number,
   dropdownItemSize: PropTypes.array,
@@ -55,4 +54,4 @@ PagingSizeUncontrolledDropdown.propTypes = {
   onChangeDropdownItem: PropTypes.func,
 };
 
-export default withTranslation()(PagingSizeUncontrolledDropdown);
+export default PagingSizeUncontrolledDropdown;

+ 8 - 3
packages/app/src/components/Admin/ElasticsearchManagement/ElasticsearchManagement.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 AdminSocketIoContainer from '~/client/services/AdminSocketIoContainer';
 import AppContainer from '~/client/services/AppContainer';
@@ -226,10 +226,15 @@ class ElasticsearchManagement extends React.Component {
 
 }
 
+const ElasticsearchManagementWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <ElasticsearchManagement t={t} {...props} />;
+};
+
 /**
  * Wrapper component for using unstated
  */
-const ElasticsearchManagementWrapper = withUnstatedContainers(ElasticsearchManagement, [AppContainer, AdminSocketIoContainer]);
+const ElasticsearchManagementWrapper = withUnstatedContainers(ElasticsearchManagementWrapperFC, [AppContainer, AdminSocketIoContainer]);
 
 ElasticsearchManagement.propTypes = {
   t: PropTypes.func.isRequired, // i18next
@@ -237,4 +242,4 @@ ElasticsearchManagement.propTypes = {
   adminSocketIoContainer: PropTypes.instanceOf(AdminSocketIoContainer).isRequired,
 };
 
-export default withTranslation()(ElasticsearchManagementWrapper);
+export default ElasticsearchManagementWrapper;

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

@@ -1,8 +1,7 @@
 import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
+import PropTypes from 'prop-types';
+import { useTranslation } from 'react-i18next';
 
 class StatusTable extends React.PureComponent {
 
@@ -161,10 +160,11 @@ class StatusTable extends React.PureComponent {
 
 }
 
-/**
- * Wrapper component for using unstated
- */
-const StatusTableWrapper = withUnstatedContainers(StatusTable, []);
+const StatusTableWrapperFC = (props) => {
+  const { t } = useTranslation();
+
+  return <StatusTable t={t} {...props} />;
+};
 
 StatusTable.propTypes = {
   t: PropTypes.func.isRequired, // i18next
@@ -179,4 +179,4 @@ StatusTable.propTypes = {
   aliasesData: PropTypes.object,
 };
 
-export default withTranslation()(StatusTableWrapper);
+export default StatusTableWrapperFC;

+ 0 - 66
packages/app/src/components/Admin/ExportArchiveData/ArchiveFilesTable.jsx

@@ -1,66 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-import { format } from 'date-fns';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-import AppContainer from '~/client/services/AppContainer';
-
-import ArchiveFilesTableMenu from './ArchiveFilesTableMenu';
-
-class ArchiveFilesTable extends React.Component {
-
-  render() {
-    const { t } = this.props;
-
-    return (
-      <div className="table-responsive">
-        <table className="table table-bordered">
-          <thead>
-            <tr>
-              <th>{t('admin:export_management.file')}</th>
-              <th>{t('admin:export_management.growi_version')}</th>
-              <th>{t('admin:export_management.collections')}</th>
-              <th>{t('admin:export_management.exported_at')}</th>
-              <th></th>
-            </tr>
-          </thead>
-          <tbody>
-            {this.props.zipFileStats.map(({ meta, fileName, innerFileStats }) => {
-              return (
-                <tr key={fileName}>
-                  <th>{fileName}</th>
-                  <td>{meta.version}</td>
-                  <td className="text-capitalize">{innerFileStats.map(fileStat => fileStat.collectionName).join(', ')}</td>
-                  <td>{meta.exportedAt ? format(new Date(meta.exportedAt), 'yyyy/MM/dd HH:mm:ss') : ''}</td>
-                  <td>
-                    <ArchiveFilesTableMenu
-                      fileName={fileName}
-                      onZipFileStatRemove={this.props.onZipFileStatRemove}
-                    />
-                  </td>
-                </tr>
-              );
-            })}
-          </tbody>
-        </table>
-      </div>
-    );
-  }
-
-}
-
-ArchiveFilesTable.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-
-  zipFileStats: PropTypes.arrayOf(PropTypes.object).isRequired,
-  onZipFileStatRemove: PropTypes.func.isRequired,
-};
-
-/**
- * Wrapper component for using unstated
- */
-const ArchiveFilesTableWrapper = withUnstatedContainers(ArchiveFilesTable, [AppContainer]);
-
-export default withTranslation()(ArchiveFilesTableWrapper);

+ 51 - 0
packages/app/src/components/Admin/ExportArchiveData/ArchiveFilesTable.tsx

@@ -0,0 +1,51 @@
+import React from 'react';
+
+import { format } from 'date-fns';
+import { useTranslation } from 'react-i18next';
+
+import ArchiveFilesTableMenu from './ArchiveFilesTableMenu';
+
+type ArchiveFilesTableProps = {
+  zipFileStats: any[],
+  onZipFileStatRemove: (fileName: string) => void,
+}
+
+const ArchiveFilesTable = (props: ArchiveFilesTableProps): JSX.Element => {
+  const { t } = useTranslation();
+
+  return (
+    <div className="table-responsive">
+      <table className="table table-bordered">
+        <thead>
+          <tr>
+            <th>{t('admin:export_management.file')}</th>
+            <th>{t('admin:export_management.growi_version')}</th>
+            <th>{t('admin:export_management.collections')}</th>
+            <th>{t('admin:export_management.exported_at')}</th>
+            <th></th>
+          </tr>
+        </thead>
+        <tbody>
+          {props.zipFileStats.map(({ meta, fileName, innerFileStats }) => {
+            return (
+              <tr key={fileName}>
+                <th>{fileName}</th>
+                <td>{meta.version}</td>
+                <td className="text-capitalize">{innerFileStats.map(fileStat => fileStat.collectionName).join(', ')}</td>
+                <td>{meta.exportedAt ? format(new Date(meta.exportedAt), 'yyyy/MM/dd HH:mm:ss') : ''}</td>
+                <td>
+                  <ArchiveFilesTableMenu
+                    fileName={fileName}
+                    onZipFileStatRemove={props.onZipFileStatRemove}
+                  />
+                </td>
+              </tr>
+            );
+          })}
+        </tbody>
+      </table>
+    </div>
+  );
+};
+
+export default ArchiveFilesTable;

+ 0 - 46
packages/app/src/components/Admin/ExportArchiveData/ArchiveFilesTableMenu.jsx

@@ -1,46 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-import AppContainer from '~/client/services/AppContainer';
-// import { toastSuccess, toastError } from '~/client/util/apiNotification';
-
-class ArchiveFilesTableMenu extends React.Component {
-
-  render() {
-    const { t } = this.props;
-
-    return (
-      <div className="btn-group admin-user-menu dropdown">
-        <button type="button" className="btn btn-sm btn-outline-secondary dropdown-toggle" data-toggle="dropdown">
-          <i className="icon-settings"></i> <span className="caret"></span>
-        </button>
-        <ul className="dropdown-menu" role="menu">
-          <li className="dropdown-header">{t('admin:export_management.export_menu')}</li>
-          <button type="button" className="dropdown-item" onClick={() => { window.location.href = `/admin/export/${this.props.fileName}` }}>
-            <i className="icon-cloud-download" /> {t('admin:export_management.download')}
-          </button>
-          <button type="button" className="dropdown-item" role="button" onClick={() => this.props.onZipFileStatRemove(this.props.fileName)}>
-            <span className="text-danger"><i className="icon-trash" /> {t('admin:export_management.delete')}</span>
-          </button>
-        </ul>
-      </div>
-    );
-  }
-
-}
-
-ArchiveFilesTableMenu.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  fileName: PropTypes.string.isRequired,
-  onZipFileStatRemove: PropTypes.func.isRequired,
-};
-
-/**
- * Wrapper component for using unstated
- */
-const ArchiveFilesTableMenuWrapper = withUnstatedContainers(ArchiveFilesTableMenu, [AppContainer]);
-
-export default withTranslation()(ArchiveFilesTableMenuWrapper);

+ 33 - 0
packages/app/src/components/Admin/ExportArchiveData/ArchiveFilesTableMenu.tsx

@@ -0,0 +1,33 @@
+import React from 'react';
+
+import { useTranslation } from 'react-i18next';
+
+// import { toastSuccess, toastError } from '~/client/util/apiNotification';
+
+type ArchiveFilesTableMenuProps = {
+  fileName: string,
+  onZipFileStatRemove: (fileName: string) => void,
+}
+
+const ArchiveFilesTableMenu = (props: ArchiveFilesTableMenuProps):JSX.Element => {
+  const { t } = useTranslation();
+
+  return (
+    <div className="btn-group admin-user-menu dropdown">
+      <button type="button" className="btn btn-sm btn-outline-secondary dropdown-toggle" data-toggle="dropdown">
+        <i className="icon-settings"></i> <span className="caret"></span>
+      </button>
+      <ul className="dropdown-menu" role="menu">
+        <li className="dropdown-header">{t('admin:export_management.export_menu')}</li>
+        <button type="button" className="dropdown-item" onClick={() => { window.location.href = `/admin/export/${props.fileName}` }}>
+          <i className="icon-cloud-download" /> {t('admin:export_management.download')}
+        </button>
+        <button type="button" className="dropdown-item" role="button" onClick={() => props.onZipFileStatRemove(props.fileName)}>
+          <span className="text-danger"><i className="icon-trash" /> {t('admin:export_management.delete')}</span>
+        </button>
+      </ul>
+    </div>
+  );
+};
+
+export default ArchiveFilesTableMenu;

+ 7 - 9
packages/app/src/components/Admin/ExportArchiveData/SelectCollectionsModal.jsx

@@ -1,16 +1,14 @@
 import React from 'react';
 
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 import * as toastr from 'toastr';
 
-import AppContainer from '~/client/services/AppContainer';
 import { apiPost } from '~/client/util/apiv1-client';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
 // import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
 
@@ -234,7 +232,6 @@ class SelectCollectionsModal extends React.Component {
 
 SelectCollectionsModal.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 
   isOpen: PropTypes.bool.isRequired,
   onExportingRequested: PropTypes.func.isRequired,
@@ -242,9 +239,10 @@ SelectCollectionsModal.propTypes = {
   collections: PropTypes.arrayOf(PropTypes.string).isRequired,
 };
 
-/**
- * Wrapper component for using unstated
- */
-const SelectCollectionsModalWrapper = withUnstatedContainers(SelectCollectionsModal, [AppContainer]);
+const SelectCollectionsModalWrapperFc = (props) => {
+  const { t } = useTranslation();
 
-export default withTranslation()(SelectCollectionsModalWrapper);
+  return <SelectCollectionsModal t={t} {...props} />;
+};
+
+export default SelectCollectionsModalWrapperFc;

+ 0 - 50
packages/app/src/components/Admin/ImportData/GrowiArchive/ErrorViewer.jsx

@@ -1,50 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-import { Modal, ModalHeader, ModalBody } from 'reactstrap';
-
-import { withUnstatedContainers } from '../../../UnstatedUtils';
-
-
-class ErrorViewer extends React.Component {
-
-  render() {
-    const { errors } = this.props;
-
-    let value = '(no errors)';
-    if (errors != null && errors.length > 0) {
-      const lines = errors.map((obj) => {
-        return JSON.stringify(obj);
-      });
-      value = lines.join('\n');
-    }
-
-    return (
-      <Modal isOpen={this.props.isOpen} toggle={this.props.onClose} size="lg">
-        <ModalHeader tag="h4" toggle={this.props.onClose} className="bg-danger text-light">
-          Errors
-        </ModalHeader>
-        <ModalBody>
-          <textarea className="form-control" rows="8" readOnly wrap="off" defaultValue={value}></textarea>
-        </ModalBody>
-      </Modal>
-    );
-  }
-
-}
-
-ErrorViewer.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-
-  isOpen: PropTypes.bool.isRequired,
-  onClose: PropTypes.func.isRequired,
-
-  errors: PropTypes.arrayOf(PropTypes.object),
-};
-
-/**
- * Wrapper component for using unstated
- */
-const ErrorViewerWrapper = withUnstatedContainers(ErrorViewer, []);
-
-export default withTranslation()(ErrorViewerWrapper);

+ 34 - 0
packages/app/src/components/Admin/ImportData/GrowiArchive/ErrorViewer.tsx

@@ -0,0 +1,34 @@
+import React from 'react';
+
+import { Modal, ModalHeader, ModalBody } from 'reactstrap';
+
+type ErrorViewerProps = {
+  isOpen: boolean,
+  errors: any[],
+  onClose: () => void,
+}
+
+const ErrorViewer = (props: ErrorViewerProps): JSX.Element => {
+  const { errors } = props;
+
+  let value = '(no errors)';
+  if (errors != null && errors.length > 0) {
+    const lines = errors.map((obj) => {
+      return JSON.stringify(obj);
+    });
+    value = lines.join('\n');
+  }
+
+  return (
+    <Modal isOpen={props.isOpen} toggle={props.onClose} size="lg">
+      <ModalHeader tag="h4" toggle={props.onClose} className="bg-danger text-light">
+        Errors
+      </ModalHeader>
+      <ModalBody>
+        <textarea className="form-control" rows={8} readOnly wrap="off" defaultValue={value}></textarea>
+      </ModalBody>
+    </Modal>
+  );
+};
+
+export default ErrorViewer;

+ 8 - 8
packages/app/src/components/Admin/ImportData/GrowiArchive/ImportCollectionConfigurationModal.jsx

@@ -1,8 +1,9 @@
 /* eslint-disable react/no-danger */
 
 import React from 'react';
+
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 import {
   Modal,
   ModalHeader,
@@ -12,8 +13,6 @@ import {
 
 import GrowiArchiveImportOption from '~/models/admin/growi-archive-import-option';
 
-import { withUnstatedContainers } from '../../../UnstatedUtils';
-import AppContainer from '~/client/services/AppContainer';
 // import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
 
@@ -233,9 +232,10 @@ ImportCollectionConfigurationModal.propTypes = {
   option: PropTypes.instanceOf(GrowiArchiveImportOption).isRequired,
 };
 
-/**
- * Wrapper component for using unstated
- */
-const ImportCollectionConfigurationModalWrapper = withUnstatedContainers(ImportCollectionConfigurationModal, [AppContainer]);
+const ImportCollectionConfigurationModalWrapperFc = (props) => {
+  const { t } = useTranslation();
+
+  return <ImportCollectionConfigurationModal t={t} {...props} />;
+};
 
-export default withTranslation()(ImportCollectionConfigurationModalWrapper);
+export default ImportCollectionConfigurationModalWrapperFc;

+ 1 - 4
packages/app/src/components/Admin/ImportData/GrowiArchive/ImportCollectionItem.jsx

@@ -1,9 +1,6 @@
 import React from 'react';
-import PropTypes from 'prop-types';
-
-// eslint-disable-next-line no-unused-vars
-import { withTranslation } from 'react-i18next';
 
+import PropTypes from 'prop-types';
 import { Progress } from 'reactstrap';
 
 import GrowiArchiveImportOption from '~/models/admin/growi-archive-import-option';

+ 10 - 6
packages/app/src/components/Admin/ImportData/GrowiArchive/ImportForm.jsx

@@ -1,10 +1,9 @@
 import React from 'react';
 
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 
 import AdminSocketIoContainer from '~/client/services/AdminSocketIoContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Post } from '~/client/util/apiv3-client';
 import GrowiArchiveImportOption from '~/models/admin/growi-archive-import-option';
@@ -290,7 +289,7 @@ class ImportForm extends React.Component {
 
   async import() {
     const {
-      appContainer, fileName, onPostImport, t,
+      fileName, onPostImport, t,
     } = this.props;
     const { selectedCollections, optionsMap } = this.state;
 
@@ -497,7 +496,6 @@ class ImportForm extends React.Component {
 
 ImportForm.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminSocketIoContainer: PropTypes.instanceOf(AdminSocketIoContainer).isRequired,
 
   fileName: PropTypes.string,
@@ -506,9 +504,15 @@ ImportForm.propTypes = {
   onPostImport: PropTypes.func,
 };
 
+const ImportFormWrapperFc = (props) => {
+  const { t } = useTranslation();
+
+  return <ImportForm t={t} {...props} />;
+};
+
 /**
  * Wrapper component for using unstated
  */
-const ImportFormWrapper = withUnstatedContainers(ImportForm, [AppContainer, AdminSocketIoContainer]);
+const ImportFormWrapper = withUnstatedContainers(ImportFormWrapperFc, [AdminSocketIoContainer]);
 
-export default withTranslation()(ImportFormWrapper);
+export default ImportFormWrapper;

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

@@ -1,13 +1,10 @@
 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 { 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,15 @@ 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]);
+const UploadFormWrapperFc = (props) => {
+  const { t } = useTranslation();
+
+  return <UploadForm t={t} {...props} />;
+};
 
-export default withTranslation()(UploadFormWrapper);
+export default UploadFormWrapperFc;

+ 7 - 9
packages/app/src/components/Admin/ImportData/GrowiArchiveSection.jsx

@@ -1,13 +1,11 @@
 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';
 
-import AppContainer from '~/client/services/AppContainer';
 import { apiv3Delete, apiv3Get } from '~/client/util/apiv3-client';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
 // import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
 import ImportForm from './GrowiArchive/ImportForm';
@@ -152,12 +150,12 @@ class GrowiArchiveSection extends React.Component {
 
 GrowiArchiveSection.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 };
 
-/**
- * Wrapper component for using unstated
- */
-const GrowiArchiveSectionWrapper = withUnstatedContainers(GrowiArchiveSection, [AppContainer]);
+const GrowiArchiveSectionWrapperFc = (props) => {
+  const { t } = useTranslation();
 
-export default withTranslation()(GrowiArchiveSectionWrapper);
+  return <GrowiArchiveSection t={t} {...props} />;
+};
+
+export default GrowiArchiveSectionWrapperFc;

+ 14 - 6
packages/app/src/components/Admin/ImportData/ImportDataPageContents.jsx

@@ -1,12 +1,14 @@
-import React, { Fragment } from 'react';
-import { withTranslation } from 'react-i18next';
+import React from 'react';
+
 import PropTypes from 'prop-types';
+import { useTranslation } from 'react-i18next';
+
+import AdminImportContainer from '~/client/services/AdminImportContainer';
+
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
 import GrowiArchiveSection from './GrowiArchiveSection';
 
-import AdminImportContainer from '~/client/services/AdminImportContainer';
-
 class ImportDataPageContents extends React.Component {
 
   render() {
@@ -237,9 +239,15 @@ ImportDataPageContents.propTypes = {
   adminImportContainer: PropTypes.instanceOf(AdminImportContainer).isRequired,
 };
 
+const ImportDataPageContentsWrapperFc = (props) => {
+  const { t } = useTranslation();
+
+  return <ImportDataPageContents t={t} {...props} />;
+};
+
 /**
  * Wrapper component for using unstated
  */
-const ImportDataPageContentsWrapper = withUnstatedContainers(ImportDataPageContents, [AdminImportContainer]);
+const ImportDataPageContentsWrapper = withUnstatedContainers(ImportDataPageContentsWrapperFc, [AdminImportContainer]);
 
-export default withTranslation()(ImportDataPageContentsWrapper);
+export default ImportDataPageContentsWrapper;

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

@@ -1,14 +1,13 @@
 import React from 'react';
+
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 
+import AdminSlackIntegrationLegacyContainer from '~/client/services/AdminSlackIntegrationLegacyContainer';
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import loggerFactory from '~/utils/logger';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
-
-import AppContainer from '~/client/services/AppContainer';
-import AdminSlackIntegrationLegacyContainer from '~/client/services/AdminSlackIntegrationLegacyContainer';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 const logger = loggerFactory('growi:slackAppConfiguration');
@@ -170,13 +169,19 @@ class SlackConfiguration extends React.Component {
 
 }
 
-const SlackConfigurationWrapper = withUnstatedContainers(SlackConfiguration, [AppContainer, AdminSlackIntegrationLegacyContainer]);
 
 SlackConfiguration.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminSlackIntegrationLegacyContainer: PropTypes.instanceOf(AdminSlackIntegrationLegacyContainer).isRequired,
 
 };
 
-export default withTranslation()(SlackConfigurationWrapper);
+const SlackConfigurationWrapperFc = (props) => {
+  const { t } = useTranslation();
+
+  return <SlackConfiguration t={t} {...props} />;
+};
+
+const SlackConfigurationWrapper = withUnstatedContainers(SlackConfigurationWrapperFc, [AdminSlackIntegrationLegacyContainer]);
+
+export default SlackConfigurationWrapper;

+ 14 - 8
packages/app/src/components/Admin/MarkdownSetting/PresentationForm.jsx

@@ -1,14 +1,14 @@
 import React from 'react';
 
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-import loggerFactory from '~/utils/logger';
+import { useTranslation } from 'react-i18next';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
+import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
+import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import loggerFactory from '~/utils/logger';
 
-import AppContainer from '~/client/services/AppContainer';
-import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
+import { withUnstatedContainers } from '../../UnstatedUtils';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 const logger = loggerFactory('growi:markdown:presentation');
@@ -127,8 +127,6 @@ class PresentationForm extends React.Component {
 
 }
 
-const PresentationFormWrapper = withUnstatedContainers(PresentationForm, [AppContainer, AdminMarkDownContainer]);
-
 PresentationForm.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
@@ -136,4 +134,12 @@ PresentationForm.propTypes = {
 
 };
 
-export default withTranslation()(PresentationFormWrapper);
+const PresentationFormWrapperFC = (props) => {
+  const { t } = useTranslation();
+
+  return <PresentationForm t={t} {...props} />;
+};
+
+const PresentationFormWrapper = withUnstatedContainers(PresentationFormWrapperFC, [AppContainer, AdminMarkDownContainer]);
+
+export default PresentationFormWrapper;

+ 14 - 6
packages/app/src/components/Admin/MarkdownSetting/WhiteListInput.jsx

@@ -1,12 +1,13 @@
 import React from 'react';
+
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
+import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
+import AppContainer from '~/client/services/AppContainer';
 import { tags, attrs } from '~/services/xss/recommended-whitelist';
 
-import AppContainer from '~/client/services/AppContainer';
-import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
+import { withUnstatedContainers } from '../../UnstatedUtils';
 
 class WhiteListInput extends React.Component {
 
@@ -75,7 +76,6 @@ class WhiteListInput extends React.Component {
 
 }
 
-const WhiteListWrapper = withUnstatedContainers(WhiteListInput, [AppContainer, AdminMarkDownContainer]);
 
 WhiteListInput.propTypes = {
   t: PropTypes.func.isRequired, // i18next
@@ -84,4 +84,12 @@ WhiteListInput.propTypes = {
 
 };
 
-export default withTranslation()(WhiteListWrapper);
+const PresentationFormWrapperFC = (props) => {
+  const { t } = useTranslation();
+
+  return <WhiteListInput t={t} {...props} />;
+};
+
+const WhiteListWrapper = withUnstatedContainers(PresentationFormWrapperFC, [AppContainer, AdminMarkDownContainer]);
+
+export default WhiteListWrapper;

+ 14 - 7
packages/app/src/components/Admin/MarkdownSetting/XssForm.jsx

@@ -1,15 +1,15 @@
 import React from 'react';
 
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-import loggerFactory from '~/utils/logger';
+import { useTranslation } from 'react-i18next';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
+import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
+import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { tags, attrs } from '~/services/xss/recommended-whitelist';
+import loggerFactory from '~/utils/logger';
 
-import AppContainer from '~/client/services/AppContainer';
-import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
+import { withUnstatedContainers } from '../../UnstatedUtils';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 import WhiteListInput from './WhiteListInput';
@@ -162,7 +162,6 @@ class XssForm extends React.Component {
 
 }
 
-const XssFormWrapper = withUnstatedContainers(XssForm, [AppContainer, AdminMarkDownContainer]);
 
 XssForm.propTypes = {
   t: PropTypes.func.isRequired, // i18next
@@ -170,4 +169,12 @@ XssForm.propTypes = {
   adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
 };
 
-export default withTranslation()(XssFormWrapper);
+const XssFormWrapperFC = (props) => {
+  const { t } = useTranslation();
+
+  return <XssForm t={t} {...props} />;
+};
+
+const XssFormWrapper = withUnstatedContainers(XssFormWrapperFC, [AppContainer, AdminMarkDownContainer]);
+
+export default XssFormWrapper;

+ 14 - 7
packages/app/src/components/Admin/Notification/GlobalNotification.jsx

@@ -1,14 +1,15 @@
 import React from 'react';
+
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 
+import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
+import AppContainer from '~/client/services/AppContainer';
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import loggerFactory from '~/utils/logger';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
-import AppContainer from '~/client/services/AppContainer';
-import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
 import GlobalNotificationList from './GlobalNotificationList';
 
 const logger = loggerFactory('growi:GlobalNotification');
@@ -127,8 +128,6 @@ class GlobalNotification extends React.Component {
 
 }
 
-const GlobalNotificationWrapper = withUnstatedContainers(GlobalNotification, [AppContainer, AdminNotificationContainer]);
-
 GlobalNotification.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
@@ -136,4 +135,12 @@ GlobalNotification.propTypes = {
 
 };
 
-export default withTranslation()(GlobalNotificationWrapper);
+const GlobalNotificationWrapperFC = (props) => {
+  const { t } = useTranslation();
+
+  return <GlobalNotification t={t} {...props} />;
+};
+
+const GlobalNotificationWrapper = withUnstatedContainers(GlobalNotificationWrapperFC, [AppContainer, AdminNotificationContainer]);
+
+export default GlobalNotificationWrapper;

+ 10 - 4
packages/app/src/components/Admin/Notification/GlobalNotificationList.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 urljoin from 'url-join';
 
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
@@ -174,8 +174,6 @@ class GlobalNotificationList extends React.Component {
 
 }
 
-const GlobalNotificationListWrapper = withUnstatedContainers(GlobalNotificationList, [AppContainer, AdminNotificationContainer]);
-
 GlobalNotificationList.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
@@ -183,4 +181,12 @@ GlobalNotificationList.propTypes = {
 
 };
 
-export default withTranslation()(GlobalNotificationListWrapper);
+const GlobalNotificationListWrapperFC = (props) => {
+  const { t } = useTranslation();
+
+  return <GlobalNotificationList t={t} {...props} />;
+};
+
+const GlobalNotificationListWrapper = withUnstatedContainers(GlobalNotificationListWrapperFC, [AppContainer, AdminNotificationContainer]);
+
+export default GlobalNotificationListWrapper;

+ 11 - 4
packages/app/src/components/Admin/Notification/ManageGlobalNotification.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 urljoin from 'url-join';
 
 import AppContainer from '~/client/services/AppContainer';
@@ -312,12 +312,19 @@ class ManageGlobalNotification extends React.Component {
 
 }
 
-const ManageGlobalNotificationWrapper = withUnstatedContainers(ManageGlobalNotification, [AppContainer]);
-
 ManageGlobalNotification.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 
 };
 
-export default withTranslation()(ManageGlobalNotificationWrapper);
+const ManageGlobalNotificationWrapperFC = (props) => {
+  const { t } = useTranslation();
+
+  return <ManageGlobalNotification t={t} {...props} />;
+};
+
+const ManageGlobalNotificationWrapper = withUnstatedContainers(ManageGlobalNotificationWrapperFC, [AppContainer]);
+
+
+export default ManageGlobalNotificationWrapper;

+ 10 - 3
packages/app/src/components/Admin/Notification/NotificationDeleteModal.jsx

@@ -1,7 +1,7 @@
 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';
@@ -43,4 +43,11 @@ NotificationDeleteModal.propTypes = {
   notificationForConfiguration: PropTypes.object.isRequired,
 };
 
-export default withTranslation()(NotificationDeleteModal);
+// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+const NotificationDeleteModalWrapperFC = (props) => {
+  const { t } = useTranslation();
+
+  return <NotificationDeleteModal t={t} {...props} />;
+};
+
+export default NotificationDeleteModalWrapperFC;

+ 9 - 2
packages/app/src/components/Admin/Notification/TriggerEventCheckBox.jsx

@@ -1,6 +1,7 @@
 import React from 'react';
+
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 
 const TriggerEventCheckBox = (props) => {
   const { t } = props;
@@ -33,5 +34,11 @@ TriggerEventCheckBox.propTypes = {
   children: PropTypes.object.isRequired,
 };
 
+// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+const TriggerEventCheckBoxWrapperFC = (props) => {
+  const { t } = useTranslation();
+
+  return <TriggerEventCheckBox t={t} {...props} />;
+};
 
-export default withTranslation()(TriggerEventCheckBox);
+export default TriggerEventCheckBoxWrapperFC;

+ 16 - 8
packages/app/src/components/Admin/Notification/UserNotificationRow.jsx

@@ -1,11 +1,12 @@
 import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
+import PropTypes from 'prop-types';
+import { useTranslation } from 'react-i18next';
 
-import AppContainer from '~/client/services/AppContainer';
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
+import AppContainer from '~/client/services/AppContainer';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
 
 import NotificationTypeIcon from './NotificationTypeIcon';
 
@@ -35,9 +36,6 @@ class UserNotificationRow extends React.PureComponent {
 
 }
 
-
-const UserNotificationRowWrapper = withUnstatedContainers(UserNotificationRow, [AppContainer, AdminNotificationContainer]);
-
 UserNotificationRow.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
@@ -47,4 +45,14 @@ UserNotificationRow.propTypes = {
   onClickDeleteBtn: PropTypes.func.isRequired,
 };
 
-export default withTranslation()(UserNotificationRowWrapper);
+// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+const UserNotificationRowWrapperWrapperFC = (props) => {
+  const { t } = useTranslation();
+
+  return <UserNotificationRow t={t} {...props} />;
+};
+
+const UserNotificationRowWrapper = withUnstatedContainers(UserNotificationRowWrapperWrapperFC, [AppContainer, AdminNotificationContainer]);
+
+
+export default UserNotificationRowWrapper;

+ 14 - 7
packages/app/src/components/Admin/Notification/UserTriggerNotification.jsx

@@ -1,14 +1,15 @@
 import React from 'react';
+
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 
+import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
+import AppContainer from '~/client/services/AppContainer';
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import loggerFactory from '~/utils/logger';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
-import AppContainer from '~/client/services/AppContainer';
-import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
 import UserNotificationRow from './UserNotificationRow';
 
 const logger = loggerFactory('growi:slackAppConfiguration');
@@ -145,8 +146,6 @@ class UserTriggerNotification extends React.Component {
 }
 
 
-const UserTriggerNotificationWrapper = withUnstatedContainers(UserTriggerNotification, [AppContainer, AdminNotificationContainer]);
-
 UserTriggerNotification.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
@@ -154,4 +153,12 @@ UserTriggerNotification.propTypes = {
 
 };
 
-export default withTranslation()(UserTriggerNotificationWrapper);
+const UserTriggerNotificationWrapperFC = (props) => {
+  const { t } = useTranslation();
+
+  return <UserTriggerNotification t={t} {...props} />;
+};
+
+const UserTriggerNotificationWrapper = withUnstatedContainers(UserTriggerNotificationWrapperFC, [AppContainer, AdminNotificationContainer]);
+
+export default UserTriggerNotificationWrapper;

+ 13 - 6
packages/app/src/components/Admin/Security/BasicSecuritySettingContents.jsx

@@ -1,13 +1,14 @@
 /* eslint-disable react/no-danger */
 import React from 'react';
+
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
+import AdminBasicSecurityContainer from '~/client/services/AdminBasicSecurityContainer';
+import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
-import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
-import AdminBasicSecurityContainer from '~/client/services/AdminBasicSecurityContainer';
+import { withUnstatedContainers } from '../../UnstatedUtils';
 
 class BasicSecurityManagementContents extends React.Component {
 
@@ -124,9 +125,15 @@ BasicSecurityManagementContents.propTypes = {
   adminBasicSecurityContainer: PropTypes.instanceOf(AdminBasicSecurityContainer).isRequired,
 };
 
-const BasicSecurityManagementContentsWrapper = withUnstatedContainers(BasicSecurityManagementContents, [
+const BasicSecurityManagementContentsWrapperFC = (props) => {
+  const { t } = useTranslation();
+
+  return <BasicSecurityManagementContents t={t} {...props} />;
+};
+
+const BasicSecurityManagementContentsWrapper = withUnstatedContainers(BasicSecurityManagementContentsWrapperFC, [
   AdminGeneralSecurityContainer,
   AdminBasicSecurityContainer,
 ]);
 
-export default withTranslation()(BasicSecurityManagementContentsWrapper);
+export default BasicSecurityManagementContentsWrapper;

+ 10 - 4
packages/app/src/components/Admin/Security/DeleteAllShareLinksModal.jsx

@@ -1,8 +1,7 @@
 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 {
   Button, Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
@@ -63,4 +62,11 @@ DeleteAllShareLinksModal.propTypes = {
   onClickDeleteButton: PropTypes.func,
 };
 
-export default withTranslation()(DeleteAllShareLinksModal);
+// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+const DeleteAllShareLinksModalWrapperFC = (props) => {
+  const { t } = useTranslation();
+
+  return <DeleteAllShareLinksModal t={t} {...props} />;
+};
+
+export default DeleteAllShareLinksModalWrapperFC;

+ 12 - 17
packages/app/src/components/Admin/Security/SecurityManagementContents.jsx

@@ -1,25 +1,24 @@
 import React, { Fragment, useMemo, useState } from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
 
+import { useTranslation } from 'react-i18next';
 import { TabContent, TabPane } from 'reactstrap';
 
+import CustomNav from '../../CustomNavigation/CustomNav';
+
+import BasicSecuritySetting from './BasicSecuritySetting';
+import FacebookSecuritySetting from './FacebookSecuritySetting';
+import GitHubSecuritySetting from './GitHubSecuritySetting';
+import GoogleSecuritySetting from './GoogleSecuritySetting';
 import LdapSecuritySetting from './LdapSecuritySetting';
 import LocalSecuritySetting from './LocalSecuritySetting';
-import SamlSecuritySetting from './SamlSecuritySetting';
 import OidcSecuritySetting from './OidcSecuritySetting';
+import SamlSecuritySetting from './SamlSecuritySetting';
 import SecuritySetting from './SecuritySetting';
-import BasicSecuritySetting from './BasicSecuritySetting';
-import GoogleSecuritySetting from './GoogleSecuritySetting';
-import GitHubSecuritySetting from './GitHubSecuritySetting';
-import TwitterSecuritySetting from './TwitterSecuritySetting';
-import FacebookSecuritySetting from './FacebookSecuritySetting';
 import ShareLinkSetting from './ShareLinkSetting';
+import TwitterSecuritySetting from './TwitterSecuritySetting';
 
-import CustomNav from '../../CustomNavigation/CustomNav';
-
-function SecurityManagementContents(props) {
-  const { t } = props;
+const SecurityManagementContents = () => {
+  const { t } = useTranslation();
 
   const [activeTab, setActiveTab] = useState('passport_local');
   const [activeComponents, setActiveComponents] = useState(new Set(['passport_local']));
@@ -144,10 +143,6 @@ function SecurityManagementContents(props) {
     </div>
   );
 
-}
-
-SecurityManagementContents.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
 };
 
-export default withTranslation()(SecurityManagementContents);
+export default SecurityManagementContents;

+ 12 - 5
packages/app/src/components/Admin/Security/ShareLinkSetting.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 AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AppContainer from '~/client/services/AppContainer';
@@ -15,7 +15,6 @@ import { withUnstatedContainers } from '../../UnstatedUtils';
 
 import DeleteAllShareLinksModal from './DeleteAllShareLinksModal';
 
-
 const Pager = (props) => {
   if (props.links.length === 0) {
     return null;
@@ -192,12 +191,20 @@ class ShareLinkSetting extends React.Component {
 
 }
 
-const ShareLinkSettingWrapper = withUnstatedContainers(ShareLinkSetting, [AppContainer, AdminGeneralSecurityContainer]);
-
 ShareLinkSetting.propTypes = {
   t: PropTypes.func.isRequired, //  i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
 };
 
-export default withTranslation()(ShareLinkSettingWrapper);
+const ShareLinkSettingWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <ShareLinkSetting t={t} {...props} />;
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const ShareLinkSettingWrapper = withUnstatedContainers(ShareLinkSettingWrapperFC, [AppContainer, AdminGeneralSecurityContainer]);
+
+export default ShareLinkSettingWrapper;

+ 17 - 8
packages/app/src/components/Admin/Security/TwitterSecuritySettingContents.jsx

@@ -1,15 +1,17 @@
 /* eslint-disable react/no-danger */
 import React from 'react';
+
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminTwitterSecurityContainer from '~/client/services/AdminTwitterSecurityContainer';
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
-class TwitterSecurityManagementContents extends React.Component {
+import { withUnstatedContainers } from '../../UnstatedUtils';
+
+class TwitterSecuritySettingContents extends React.Component {
 
   constructor(props) {
     super(props);
@@ -191,16 +193,23 @@ class TwitterSecurityManagementContents extends React.Component {
 
 }
 
-
-TwitterSecurityManagementContents.propTypes = {
+TwitterSecuritySettingContents.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
   adminTwitterSecurityContainer: PropTypes.instanceOf(AdminTwitterSecurityContainer).isRequired,
 };
 
-const TwitterSecurityManagementContentsWrapper = withUnstatedContainers(TwitterSecurityManagementContents, [
+const TwitterSecuritySettingContentsWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <TwitterSecuritySettingContents t={t} {...props} />;
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const TwitterSecuritySettingContentsWrapper = withUnstatedContainers(TwitterSecuritySettingContentsWrapperFC, [
   AdminGeneralSecurityContainer,
   AdminTwitterSecurityContainer,
 ]);
 
-export default withTranslation()(TwitterSecurityManagementContentsWrapper);
+export default TwitterSecuritySettingContentsWrapper;

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

@@ -1,7 +1,8 @@
 
 import React from 'react';
+
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 
 class CheckBoxForSerchUserOption extends React.Component {
 
@@ -25,7 +26,6 @@ class CheckBoxForSerchUserOption extends React.Component {
 
 }
 
-
 CheckBoxForSerchUserOption.propTypes = {
   t: PropTypes.func.isRequired, // i18next
 
@@ -34,4 +34,9 @@ CheckBoxForSerchUserOption.propTypes = {
   onChange: PropTypes.func.isRequired,
 };
 
-export default withTranslation()(CheckBoxForSerchUserOption);
+const CheckBoxForSerchUserOptionWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <CheckBoxForSerchUserOption t={t} {...props} />;
+};
+
+export default CheckBoxForSerchUserOptionWrapperFC;

+ 14 - 11
packages/app/src/components/Admin/UserManagement.jsx

@@ -1,18 +1,17 @@
-import React, { Fragment } from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import React from 'react';
 
-import PaginationWrapper from '../PaginationWrapper';
+import PropTypes from 'prop-types';
+import { useTranslation } from 'react-i18next';
 
+import AdminUsersContainer from '~/client/services/AdminUsersContainer';
+import { toastError } from '~/client/util/apiNotification';
 
+import PaginationWrapper from '../PaginationWrapper';
 import { withUnstatedContainers } from '../UnstatedUtils';
-import { toastError } from '~/client/util/apiNotification';
 
-import AppContainer from '~/client/services/AppContainer';
-import AdminUsersContainer from '~/client/services/AdminUsersContainer';
 
-import PasswordResetModal from './Users/PasswordResetModal';
 import InviteUserControl from './Users/InviteUserControl';
+import PasswordResetModal from './Users/PasswordResetModal';
 import UserTable from './Users/UserTable';
 
 class UserManagement extends React.Component {
@@ -221,10 +220,14 @@ class UserManagement extends React.Component {
 
 UserManagement.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminUsersContainer: PropTypes.instanceOf(AdminUsersContainer).isRequired,
 };
 
-const UserManagementWrapper = withUnstatedContainers(UserManagement, [AppContainer, AdminUsersContainer]);
+const UserManagementFc = (props) => {
+  const { t } = useTranslation();
+  return <UserManagement t={t} {...props} />;
+};
+
+const UserManagementWrapper = withUnstatedContainers(UserManagementFc, [AdminUsersContainer]);
 
-export default withTranslation()(UserManagementWrapper);
+export default UserManagementWrapper;

+ 17 - 6
packages/app/src/components/Admin/Users/InviteUserControl.jsx

@@ -1,10 +1,13 @@
 import React, { Fragment } from 'react';
+
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
-import AppContainer from '~/client/services/AppContainer';
 import AdminUsersContainer from '~/client/services/AdminUsersContainer';
+import AppContainer from '~/client/services/AppContainer';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
+
 import UserInviteModal from './UserInviteModal';
 
 class InviteUserControl extends React.Component {
@@ -24,12 +27,20 @@ class InviteUserControl extends React.Component {
 
 }
 
-const InviteUserControlWrapper = withUnstatedContainers(InviteUserControl, [AppContainer, AdminUsersContainer]);
-
 InviteUserControl.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminUsersContainer: PropTypes.instanceOf(AdminUsersContainer).isRequired,
 };
 
-export default withTranslation()(InviteUserControlWrapper);
+const InviteUserControlWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <InviteUserControl t={t} {...props} />;
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const InviteUserControlWrapper = withUnstatedContainers(InviteUserControlWrapperFC, [AppContainer, AdminUsersContainer]);
+
+export default InviteUserControlWrapper;

+ 16 - 9
packages/app/src/components/Admin/Users/UserRemoveButton.jsx

@@ -1,12 +1,14 @@
 import React from 'react';
+
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
-import AppContainer from '~/client/services/AppContainer';
 import AdminUsersContainer from '~/client/services/AdminUsersContainer';
+import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
+import { withUnstatedContainers } from '../../UnstatedUtils';
+
 class UserRemoveButton extends React.Component {
 
   constructor(props) {
@@ -40,11 +42,6 @@ class UserRemoveButton extends React.Component {
 
 }
 
-/**
- * Wrapper component for using unstated
- */
-const UserRemoveButtonWrapper = withUnstatedContainers(UserRemoveButton, [AppContainer, AdminUsersContainer]);
-
 UserRemoveButton.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
@@ -53,4 +50,14 @@ UserRemoveButton.propTypes = {
   user: PropTypes.object.isRequired,
 };
 
-export default withTranslation()(UserRemoveButtonWrapper);
+const UserRemoveButtonWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <UserRemoveButton t={t} {...props} />;
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const UserRemoveButtonWrapper = withUnstatedContainers(UserRemoveButtonWrapperFC, [AppContainer, AdminUsersContainer]);
+
+export default UserRemoveButtonWrapper;

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

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

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

@@ -5,6 +5,7 @@ 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,12 +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();
-  return <InstallerForm t={t} {...props} />;
+  const { data: csrfToken } = useCsrfToken();
+
+  return <InstallerForm t={t} csrfToken={csrfToken} {...props} />;
 };
 
 export default InstallerFormWrapperFC;

+ 11 - 6
packages/app/src/components/LoginForm.jsx

@@ -5,6 +5,7 @@ import ReactCardFlip from 'react-card-flip';
 import { useTranslation } from 'react-i18next';
 
 import AppContainer from '~/client/services/AppContainer';
+import { useCsrfToken } from '~/stores/context';
 
 import { withUnstatedContainers } from './UnstatedUtils';
 
@@ -36,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">
@@ -71,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">
@@ -149,6 +150,7 @@ class LoginForm extends React.Component {
     const {
       t,
       appContainer,
+      csrfToken,
       isEmailAuthenticationEnabled,
       username,
       name,
@@ -252,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">
@@ -333,6 +335,7 @@ LoginForm.propTypes = {
   t: PropTypes.func.isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 
+  csrfToken: PropTypes.string,
   isRegistering: PropTypes.bool,
   username: PropTypes.string,
   name: PropTypes.string,
@@ -349,7 +352,9 @@ LoginForm.propTypes = {
 
 const LoginFormWrapperFC = (props) => {
   const { t } = useTranslation();
-  return <LoginForm t={t} {...props} />;
+  const { data: csrfToken } = useCsrfToken();
+
+  return <LoginForm t={t} csrfToken={csrfToken} {...props} />;
 };
 
 /**

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

+ 1 - 1
packages/app/src/components/MyDraftList/Draft.jsx

@@ -59,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);

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

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

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

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

+ 1 - 9
packages/app/src/components/PasswordResetExecutionForm.jsx

@@ -1,21 +1,16 @@
 import React, { useState } from 'react';
 
-import PropTypes from 'prop-types';
 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 } = useTranslation();
-  const { appContainer } = props;
 
   const [newPassword, setNewPassword] = useState('');
   const [newPasswordConfirm, setNewPasswordConfirm] = useState('');
@@ -91,10 +86,7 @@ const PasswordResetExecutionForm = (props) => {
   );
 };
 
-const PasswordResetExecutionFormWrapper = withUnstatedContainers(PasswordResetExecutionForm, [AppContainer]);
-
 PasswordResetExecutionForm.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 };
 
-export default PasswordResetExecutionFormWrapper;
+export default PasswordResetExecutionForm;

+ 1 - 12
packages/app/src/components/PasswordResetRequestForm.jsx

@@ -1,18 +1,13 @@
 import React, { useState } from 'react';
 
-import PropTypes from 'prop-types';
 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 } = useTranslation();
-  const { appContainer } = props;
   const [email, setEmail] = useState('');
 
   const changeEmail = (inputValue) => {
@@ -57,13 +52,7 @@ const PasswordResetRequestForm = (props) => {
   );
 };
 
-/**
- * Wrapper component for using unstated
- */
-const PasswordResetRequestFormWrapper = withUnstatedContainers(PasswordResetRequestForm, [AppContainer]);
-
 PasswordResetRequestForm.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 };
 
-export default PasswordResetRequestFormWrapper;
+export default PasswordResetRequestForm;

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