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

Merge pull request #6076 from weseek/imprv/integrate-convert-to-useTranslation

imprv: Integrate replace to useTranslation all files
Yuki Takei 3 лет назад
Родитель
Сommit
fea3776ed1
100 измененных файлов с 2243 добавлено и 2160 удалено
  1. 39 41
      packages/app/src/components/Admin/AdminHome/InstalledPluginTable.jsx
  2. 0 59
      packages/app/src/components/Admin/AdminHome/SystemInfomationTable.jsx
  3. 53 0
      packages/app/src/components/Admin/AdminHome/SystemInfomationTable.tsx
  4. 139 145
      packages/app/src/components/Admin/App/AppSetting.jsx
  5. 0 113
      packages/app/src/components/Admin/App/AppSettingsPageContents.jsx
  6. 108 0
      packages/app/src/components/Admin/App/AppSettingsPageContents.tsx
  7. 20 24
      packages/app/src/components/Admin/App/FileUploadSetting.tsx
  8. 10 11
      packages/app/src/components/Admin/App/GcsSettings.jsx
  9. 16 18
      packages/app/src/components/Admin/App/MailSetting.tsx
  10. 0 79
      packages/app/src/components/Admin/App/PluginSetting.jsx
  11. 66 0
      packages/app/src/components/Admin/App/PluginSetting.tsx
  12. 10 16
      packages/app/src/components/Admin/App/SesSetting.tsx
  13. 0 105
      packages/app/src/components/Admin/App/SiteUrlSetting.jsx
  14. 93 0
      packages/app/src/components/Admin/App/SiteUrlSetting.tsx
  15. 14 17
      packages/app/src/components/Admin/App/SmtpSetting.tsx
  16. 8 5
      packages/app/src/components/Admin/Common/AdminNavigation.jsx
  17. 0 23
      packages/app/src/components/Admin/Common/AdminUpdateButtonRow.jsx
  18. 23 0
      packages/app/src/components/Admin/Common/AdminUpdateButtonRow.tsx
  19. 9 12
      packages/app/src/components/Admin/Common/LabeledProgressBar.tsx
  20. 0 79
      packages/app/src/components/Admin/Customize/CustomizeCssSetting.jsx
  21. 68 0
      packages/app/src/components/Admin/Customize/CustomizeCssSetting.tsx
  22. 0 39
      packages/app/src/components/Admin/Customize/CustomizeFunctionOption.jsx
  23. 37 0
      packages/app/src/components/Admin/Customize/CustomizeFunctionOption.tsx
  24. 0 174
      packages/app/src/components/Admin/Customize/CustomizeFunctionSetting.jsx
  25. 163 0
      packages/app/src/components/Admin/Customize/CustomizeFunctionSetting.tsx
  26. 0 89
      packages/app/src/components/Admin/Customize/CustomizeHeaderSetting.jsx
  27. 76 0
      packages/app/src/components/Admin/Customize/CustomizeHeaderSetting.tsx
  28. 0 156
      packages/app/src/components/Admin/Customize/CustomizeHighlightSetting.jsx
  29. 145 0
      packages/app/src/components/Admin/Customize/CustomizeHighlightSetting.tsx
  30. 4 12
      packages/app/src/components/Admin/Customize/CustomizeLayoutSetting.tsx
  31. 0 120
      packages/app/src/components/Admin/Customize/CustomizeScriptSetting.jsx
  32. 107 0
      packages/app/src/components/Admin/Customize/CustomizeScriptSetting.tsx
  33. 78 80
      packages/app/src/components/Admin/Customize/CustomizeThemeOptions.jsx
  34. 0 72
      packages/app/src/components/Admin/Customize/CustomizeThemeSetting.jsx
  35. 58 0
      packages/app/src/components/Admin/Customize/CustomizeThemeSetting.tsx
  36. 2 3
      packages/app/src/components/Admin/Customize/PagingSizeUncontrolledDropdown.jsx
  37. 8 3
      packages/app/src/components/Admin/ElasticsearchManagement/ElasticsearchManagement.jsx
  38. 0 47
      packages/app/src/components/Admin/ElasticsearchManagement/NormalizeIndicesControls.jsx
  39. 35 0
      packages/app/src/components/Admin/ElasticsearchManagement/NormalizeIndicesControls.tsx
  40. 10 6
      packages/app/src/components/Admin/ElasticsearchManagement/RebuildIndexControls.jsx
  41. 0 46
      packages/app/src/components/Admin/ElasticsearchManagement/ReconnectControls.jsx
  42. 36 0
      packages/app/src/components/Admin/ElasticsearchManagement/ReconnectControls.tsx
  43. 8 8
      packages/app/src/components/Admin/ElasticsearchManagement/StatusTable.jsx
  44. 0 66
      packages/app/src/components/Admin/ExportArchiveData/ArchiveFilesTable.jsx
  45. 51 0
      packages/app/src/components/Admin/ExportArchiveData/ArchiveFilesTable.tsx
  46. 0 46
      packages/app/src/components/Admin/ExportArchiveData/ArchiveFilesTableMenu.jsx
  47. 33 0
      packages/app/src/components/Admin/ExportArchiveData/ArchiveFilesTableMenu.tsx
  48. 7 9
      packages/app/src/components/Admin/ExportArchiveData/SelectCollectionsModal.jsx
  49. 0 50
      packages/app/src/components/Admin/ImportData/GrowiArchive/ErrorViewer.jsx
  50. 34 0
      packages/app/src/components/Admin/ImportData/GrowiArchive/ErrorViewer.tsx
  51. 8 8
      packages/app/src/components/Admin/ImportData/GrowiArchive/ImportCollectionConfigurationModal.jsx
  52. 1 4
      packages/app/src/components/Admin/ImportData/GrowiArchive/ImportCollectionItem.jsx
  53. 10 6
      packages/app/src/components/Admin/ImportData/GrowiArchive/ImportForm.jsx
  54. 8 2
      packages/app/src/components/Admin/ImportData/GrowiArchive/UploadForm.jsx
  55. 7 9
      packages/app/src/components/Admin/ImportData/GrowiArchiveSection.jsx
  56. 14 6
      packages/app/src/components/Admin/ImportData/ImportDataPageContents.jsx
  57. 13 8
      packages/app/src/components/Admin/LegacySlackIntegration/SlackConfiguration.jsx
  58. 17 18
      packages/app/src/components/Admin/MarkdownSetting/IndentForm.tsx
  59. 12 10
      packages/app/src/components/Admin/MarkdownSetting/LineBreakForm.jsx
  60. 0 55
      packages/app/src/components/Admin/MarkdownSetting/MarkDownSettingContents.jsx
  61. 52 0
      packages/app/src/components/Admin/MarkdownSetting/MarkDownSettingContents.tsx
  62. 14 8
      packages/app/src/components/Admin/MarkdownSetting/PresentationForm.jsx
  63. 14 6
      packages/app/src/components/Admin/MarkdownSetting/WhiteListInput.jsx
  64. 14 7
      packages/app/src/components/Admin/MarkdownSetting/XssForm.jsx
  65. 14 7
      packages/app/src/components/Admin/Notification/GlobalNotification.jsx
  66. 10 4
      packages/app/src/components/Admin/Notification/GlobalNotificationList.jsx
  67. 11 4
      packages/app/src/components/Admin/Notification/ManageGlobalNotification.jsx
  68. 10 3
      packages/app/src/components/Admin/Notification/NotificationDeleteModal.jsx
  69. 9 2
      packages/app/src/components/Admin/Notification/TriggerEventCheckBox.jsx
  70. 16 8
      packages/app/src/components/Admin/Notification/UserNotificationRow.jsx
  71. 14 7
      packages/app/src/components/Admin/Notification/UserTriggerNotification.jsx
  72. 13 6
      packages/app/src/components/Admin/Security/BasicSecuritySettingContents.jsx
  73. 10 4
      packages/app/src/components/Admin/Security/DeleteAllShareLinksModal.jsx
  74. 18 9
      packages/app/src/components/Admin/Security/GitHubSecuritySettingContents.jsx
  75. 8 6
      packages/app/src/components/Admin/Security/GoogleSecuritySettingContents.jsx
  76. 8 5
      packages/app/src/components/Admin/Security/LdapAuthTest.jsx
  77. 3 10
      packages/app/src/components/Admin/Security/LdapAuthTestModal.jsx
  78. 13 9
      packages/app/src/components/Admin/Security/LdapSecuritySettingContents.jsx
  79. 13 6
      packages/app/src/components/Admin/Security/LocalSecuritySettingContents.jsx
  80. 12 8
      packages/app/src/components/Admin/Security/OidcSecuritySettingContents.jsx
  81. 12 9
      packages/app/src/components/Admin/Security/SamlSecuritySettingContents.jsx
  82. 12 17
      packages/app/src/components/Admin/Security/SecurityManagementContents.jsx
  83. 13 9
      packages/app/src/components/Admin/Security/SecuritySetting.jsx
  84. 12 5
      packages/app/src/components/Admin/Security/ShareLinkSetting.jsx
  85. 17 8
      packages/app/src/components/Admin/Security/TwitterSecuritySettingContents.jsx
  86. 4 6
      packages/app/src/components/Admin/SlackIntegration/DeleteSlackBotSettingsModal.jsx
  87. 8 3
      packages/app/src/components/Admin/UserGroupDetail/CheckBoxForSerchUserOption.jsx
  88. 23 25
      packages/app/src/components/Admin/UserGroupDetail/RadioButtonForSerchUserOption.jsx
  89. 8 3
      packages/app/src/components/Admin/UserGroupDetail/UserGroupPageList.jsx
  90. 8 3
      packages/app/src/components/Admin/UserGroupDetail/UserGroupUserFormByInput.jsx
  91. 14 7
      packages/app/src/components/Admin/UserGroupDetail/UserGroupUserModal.jsx
  92. 14 7
      packages/app/src/components/Admin/UserGroupDetail/UserGroupUserTable.jsx
  93. 14 11
      packages/app/src/components/Admin/UserManagement.jsx
  94. 13 7
      packages/app/src/components/Admin/Users/ExternalAccountTable.jsx
  95. 12 5
      packages/app/src/components/Admin/Users/GiveAdminButton.jsx
  96. 17 6
      packages/app/src/components/Admin/Users/InviteUserControl.jsx
  97. 8 3
      packages/app/src/components/Admin/Users/PasswordResetModal.jsx
  98. 86 0
      packages/app/src/components/Admin/Users/RemoveAdminButton.jsx
  99. 2 3
      packages/app/src/components/Admin/Users/SortIcons.jsx
  100. 12 5
      packages/app/src/components/Admin/Users/StatusActivateButton.jsx

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

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

+ 8 - 5
packages/app/src/components/Admin/Common/AdminNavigation.jsx

@@ -1,18 +1,22 @@
 /* eslint-disable no-multi-spaces */
 /* eslint-disable react/jsx-props-no-multi-spaces */
 
+
 import React from 'react';
+
+import { pathUtils } from '@growi/core';
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 import urljoin from 'url-join';
 
-import { pathUtils } from '@growi/core';
 
 import AppContainer from '~/client/services/AppContainer';
+
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
 const AdminNavigation = (props) => {
-  const { t, appContainer } = props;
+  const { t } = useTranslation();
+  const { appContainer } = props;
   const pathname = window.location.pathname;
 
   const growiCloudUri = appContainer.config.env.GROWI_CLOUD_URI;
@@ -141,8 +145,7 @@ const AdminNavigation = (props) => {
 const AdminNavigationWrapper = withUnstatedContainers(AdminNavigation, [AppContainer]);
 
 AdminNavigation.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 };
 
-export default withTranslation()(AdminNavigationWrapper);
+export default AdminNavigationWrapper;

+ 0 - 23
packages/app/src/components/Admin/Common/AdminUpdateButtonRow.jsx

@@ -1,23 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-const AdminUpdateButtonRow = (props) => {
-  const { t } = props;
-
-  return (
-    <div className="row my-3">
-      <div className="mx-auto">
-        <button type="button" className="btn btn-primary" onClick={props.onClick} disabled={props.disabled}>{ t('Update') }</button>
-      </div>
-    </div>
-  );
-};
-
-AdminUpdateButtonRow.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  onClick: PropTypes.func.isRequired,
-  disabled: PropTypes.bool.isRequired,
-};
-
-export default withTranslation()(AdminUpdateButtonRow);

+ 23 - 0
packages/app/src/components/Admin/Common/AdminUpdateButtonRow.tsx

@@ -0,0 +1,23 @@
+import React from 'react';
+
+import { useTranslation } from 'react-i18next';
+
+type Props = {
+  onClick: () => void,
+  disabled: boolean,
+
+}
+
+const AdminUpdateButtonRow = (props: Props): JSX.Element => {
+  const { t } = useTranslation();
+
+  return (
+    <div className="row my-3">
+      <div className="mx-auto">
+        <button type="button" className="btn btn-primary" onClick={props.onClick} disabled={props.disabled}>{ t('Update') }</button>
+      </div>
+    </div>
+  );
+};
+
+export default AdminUpdateButtonRow;

+ 9 - 12
packages/app/src/components/Admin/Common/LabeledProgressBar.jsx → packages/app/src/components/Admin/Common/LabeledProgressBar.tsx

@@ -1,11 +1,16 @@
 import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
 
 import { Progress } from 'reactstrap';
 
-const LabeledProgressBar = (props) => {
+type Props = {
+  header: string,
+  currentCount: number,
+  totalCount: number,
+  errorsCount?: number,
+  isInProgress?: boolean,
+}
 
+const LabeledProgressBar = (props: Props): JSX.Element => {
   const {
     header, currentCount, totalCount, errorsCount, isInProgress,
   } = props;
@@ -27,12 +32,4 @@ const LabeledProgressBar = (props) => {
 
 };
 
-LabeledProgressBar.propTypes = {
-  header: PropTypes.string.isRequired,
-  currentCount: PropTypes.number.isRequired,
-  totalCount: PropTypes.number.isRequired,
-  errorsCount: PropTypes.number,
-  isInProgress: PropTypes.bool,
-};
-
-export default withTranslation()(LabeledProgressBar);
+export default LabeledProgressBar;

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

+ 0 - 47
packages/app/src/components/Admin/ElasticsearchManagement/NormalizeIndicesControls.jsx

@@ -1,47 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-
-class NormalizeIndicesControls extends React.PureComponent {
-
-  render() {
-    const { t, isNormalized, isRebuildingProcessing } = this.props;
-
-    const isEnabled = (isNormalized != null) && !isNormalized && !isRebuildingProcessing;
-
-    return (
-      <>
-        <button
-          type="submit"
-          className={`btn ${isEnabled ? 'btn-outline-info' : 'btn-outline-secondary'}`}
-          onClick={() => { this.props.onNormalizingRequested() }}
-          disabled={!isEnabled}
-        >
-          { t('full_text_search_management.normalize_button') }
-        </button>
-
-        <p className="form-text text-muted">
-          { t('full_text_search_management.normalize_description') }<br />
-        </p>
-      </>
-    );
-  }
-
-}
-
-/**
- * Wrapper component for using unstated
- */
-const NormalizeIndicesControlsWrapper = withUnstatedContainers(NormalizeIndicesControls, []);
-
-NormalizeIndicesControls.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-
-  isRebuildingProcessing: PropTypes.bool.isRequired,
-  onNormalizingRequested: PropTypes.func.isRequired,
-  isNormalized: PropTypes.bool,
-};
-
-export default withTranslation()(NormalizeIndicesControlsWrapper);

+ 35 - 0
packages/app/src/components/Admin/ElasticsearchManagement/NormalizeIndicesControls.tsx

@@ -0,0 +1,35 @@
+import React from 'react';
+
+import { useTranslation } from 'react-i18next';
+
+type Props = {
+  isRebuildingProcessing: boolean,
+  onNormalizingRequested: () => void,
+  isNormalized?: boolean,
+}
+
+const NormalizeIndicesControls = (props: Props): JSX.Element => {
+  const { t } = useTranslation();
+  const { isNormalized, isRebuildingProcessing } = props;
+
+  const isEnabled = (isNormalized != null) && !isNormalized && !isRebuildingProcessing;
+
+  return (
+    <>
+      <button
+        type="submit"
+        className={`btn ${isEnabled ? 'btn-outline-info' : 'btn-outline-secondary'}`}
+        onClick={() => { props.onNormalizingRequested() }}
+        disabled={!isEnabled}
+      >
+        { t('full_text_search_management.normalize_button') }
+      </button>
+
+      <p className="form-text text-muted">
+        { t('full_text_search_management.normalize_description') }<br />
+      </p>
+    </>
+  );
+};
+
+export default NormalizeIndicesControls;

+ 10 - 6
packages/app/src/components/Admin/ElasticsearchManagement/RebuildIndexControls.jsx

@@ -1,11 +1,11 @@
 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 AdminSocketIoContainer from '~/client/services/AdminSocketIoContainer';
 
+import { withUnstatedContainers } from '../../UnstatedUtils';
 import LabeledProgressBar from '../Common/LabeledProgressBar';
 
 class RebuildIndexControls extends React.Component {
@@ -107,15 +107,19 @@ class RebuildIndexControls extends React.Component {
 
 }
 
+const RebuildIndexControlsFC = (props) => {
+  const { t } = useTranslation();
+  return <RebuildIndexControls t={t} {...props} />;
+};
+
 
 /**
  * Wrapper component for using unstated
  */
-const RebuildIndexControlsWrapper = withUnstatedContainers(RebuildIndexControls, [AppContainer, AdminSocketIoContainer]);
+const RebuildIndexControlsWrapper = withUnstatedContainers(RebuildIndexControlsFC, [AdminSocketIoContainer]);
 
 RebuildIndexControls.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminSocketIoContainer: PropTypes.instanceOf(AdminSocketIoContainer).isRequired,
 
   isRebuildingProcessing: PropTypes.bool.isRequired,
@@ -125,4 +129,4 @@ RebuildIndexControls.propTypes = {
   onRebuildingRequested: PropTypes.func.isRequired,
 };
 
-export default withTranslation()(RebuildIndexControlsWrapper);
+export default RebuildIndexControlsWrapper;

+ 0 - 46
packages/app/src/components/Admin/ElasticsearchManagement/ReconnectControls.jsx

@@ -1,46 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-
-class ReconnectControls extends React.PureComponent {
-
-  render() {
-    const { t, isEnabled, isProcessing } = this.props;
-
-    return (
-      <>
-        <button
-          type="submit"
-          className={`btn ${isEnabled ? 'btn-outline-success' : 'btn-outline-secondary'}`}
-          onClick={() => { this.props.onReconnectingRequested() }}
-          disabled={!isEnabled}
-        >
-          { isProcessing && <i className="fa fa-spinner fa-pulse mr-2"></i> }
-          { t('full_text_search_management.reconnect_button') }
-        </button>
-
-        <p className="form-text text-muted">
-          { t('full_text_search_management.reconnect_description') }<br />
-        </p>
-      </>
-    );
-  }
-
-}
-
-/**
- * Wrapper component for using unstated
- */
-const ReconnectControlsWrapper = withUnstatedContainers(ReconnectControls, []);
-
-ReconnectControls.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-
-  isEnabled: PropTypes.bool,
-  isProcessing: PropTypes.bool,
-  onReconnectingRequested: PropTypes.func.isRequired,
-};
-
-export default withTranslation()(ReconnectControlsWrapper);

+ 36 - 0
packages/app/src/components/Admin/ElasticsearchManagement/ReconnectControls.tsx

@@ -0,0 +1,36 @@
+import React from 'react';
+
+import { useTranslation } from 'react-i18next';
+
+type Props = {
+  isEnabled?: boolean,
+  isProcessing?: boolean,
+  onReconnectingRequested: () => void,
+}
+
+const ReconnectControls = (props: Props): JSX.Element => {
+  const { t } = useTranslation();
+
+  const { isEnabled, isProcessing } = props;
+
+  return (
+    <>
+      <button
+        type="submit"
+        className={`btn ${isEnabled ? 'btn-outline-success' : 'btn-outline-secondary'}`}
+        onClick={() => { props.onReconnectingRequested() }}
+        disabled={!isEnabled}
+      >
+        { isProcessing && <i className="fa fa-spinner fa-pulse mr-2"></i> }
+        { t('full_text_search_management.reconnect_button') }
+      </button>
+
+      <p className="form-text text-muted">
+        { t('full_text_search_management.reconnect_description') }<br />
+      </p>
+    </>
+  );
+
+};
+
+export default ReconnectControls;

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

+ 8 - 2
packages/app/src/components/Admin/ImportData/GrowiArchive/UploadForm.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 { toastError } from '~/client/util/apiNotification';
 import { apiv3PostForm } from '~/client/util/apiv3-client';
@@ -97,4 +97,10 @@ UploadForm.propTypes = {
   onVersionMismatch: PropTypes.func,
 };
 
-export default withTranslation()(UploadForm);
+const UploadFormWrapperFc = (props) => {
+  const { t } = useTranslation();
+
+  return <UploadForm t={t} {...props} />;
+};
+
+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;

+ 17 - 18
packages/app/src/components/Admin/MarkdownSetting/IndentForm.jsx → packages/app/src/components/Admin/MarkdownSetting/IndentForm.tsx

@@ -1,25 +1,29 @@
 /* eslint-disable react/no-danger */
-import React from 'react';
+import React, { useCallback } from 'react';
 
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 import {
   UncontrolledDropdown, DropdownToggle, DropdownMenu, DropdownItem,
 } from 'reactstrap';
-import loggerFactory from '~/utils/logger';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
+import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import loggerFactory from '~/utils/logger';
 
-import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
+import { withUnstatedContainers } from '../../UnstatedUtils';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 const logger = loggerFactory('growi:importer');
 
-const IndentForm = (props) => {
-  const onClickSubmit = async(props) => {
-    const { t } = props;
 
+type Props = {
+  adminMarkDownContainer: AdminMarkDownContainer;
+}
+
+const IndentForm = (props: Props) => {
+  const { t } = useTranslation();
+
+  const onClickSubmit = useCallback(async(props) => {
     try {
       await props.adminMarkDownContainer.updateIndentSetting();
       toastSuccess(t('toaster.update_successed', { target: t('admin:markdown_setting.indent_header') }));
@@ -28,10 +32,10 @@ const IndentForm = (props) => {
       toastError(err);
       logger.error(err);
     }
-  };
+  }, [t]);
 
   const renderIndentSizeOption = (props) => {
-    const { t, adminMarkDownContainer } = props;
+    const { adminMarkDownContainer } = props;
     const { adminPreferredIndentSize } = adminMarkDownContainer.state;
 
     return (
@@ -63,7 +67,7 @@ const IndentForm = (props) => {
   };
 
   const renderIndentForceOption = (props) => {
-    const { t, adminMarkDownContainer } = props;
+    const { adminMarkDownContainer } = props;
     const { isIndentSizeForced } = adminMarkDownContainer.state;
 
     const helpIndentInComment = { __html: t('admin:markdown_setting.indent_options.disallow_indent_change_desc') };
@@ -107,9 +111,4 @@ const IndentForm = (props) => {
  */
 const IndentFormWrapper = withUnstatedContainers(IndentForm, [AdminMarkDownContainer]);
 
-IndentForm.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
-};
-
-export default withTranslation()(IndentFormWrapper);
+export default IndentFormWrapper;

+ 12 - 10
packages/app/src/components/Admin/MarkdownSetting/LineBreakForm.jsx

@@ -2,15 +2,13 @@
 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 { 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:importer');
@@ -103,15 +101,19 @@ class LineBreakForm extends React.Component {
 
 }
 
+const LineBreakFormFC = (props) => {
+  const { t } = useTranslation();
+  return <LineBreakForm t={t} {...props} />;
+};
+
 /**
  * Wrapper component for using unstated
  */
-const LineBreakFormWrapper = withUnstatedContainers(LineBreakForm, [AppContainer, AdminMarkDownContainer]);
+const LineBreakFormWrapper = withUnstatedContainers(LineBreakFormFC, [AdminMarkDownContainer]);
 
 LineBreakForm.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  t: PropTypes.func.isRequired,
   adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
 };
 
-export default withTranslation()(LineBreakFormWrapper);
+export default LineBreakFormWrapper;

+ 0 - 55
packages/app/src/components/Admin/MarkdownSetting/MarkDownSettingContents.jsx

@@ -1,55 +0,0 @@
-import React from 'react';
-import { Card, CardBody } from 'reactstrap';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import LineBreakForm from './LineBreakForm';
-import IndentForm from './IndentForm';
-import PresentationForm from './PresentationForm';
-import XssForm from './XssForm';
-
-
-class MarkDownSettingContents extends React.Component {
-
-  render() {
-    const { t } = this.props;
-    return (
-      <div data-testid="admin-markdown">
-        {/* Line Break Setting */}
-        <h2 className="admin-setting-header">{t('admin:markdown_setting.lineBreak_header')}</h2>
-        <Card className="card well my-3">
-          <CardBody className="px-0 py-2">{ t('admin:markdown_setting.lineBreak_desc') }</CardBody>
-        </Card>
-        <LineBreakForm />
-
-        {/* Indent Setting */}
-        <h2 className="admin-setting-header">{t('admin:markdown_setting.indent_header')}</h2>
-        <Card className="card well my-3">
-          <CardBody className="px-0 py-2">{t('admin:markdown_setting.indent_desc') }</CardBody>
-        </Card>
-        <IndentForm />
-
-        {/* Presentation Setting */}
-        <h2 className="admin-setting-header">{ t('admin:markdown_setting.presentation_header') }</h2>
-        <Card className="card well my-3">
-          <CardBody className="px-0 py-2">{ t('admin:markdown_setting.presentation_desc') }</CardBody>
-        </Card>
-        <PresentationForm />
-
-        {/* XSS Setting */}
-        <h2 className="admin-setting-header">{ t('admin:markdown_setting.xss_header') }</h2>
-        <Card className="card well my-3">
-          <CardBody className="px-0 py-2">{ t('admin:markdown_setting.xss_desc') }</CardBody>
-        </Card>
-        <XssForm />
-      </div>
-    );
-  }
-
-}
-
-MarkDownSettingContents.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-};
-
-export default withTranslation()(MarkDownSettingContents);

+ 52 - 0
packages/app/src/components/Admin/MarkdownSetting/MarkDownSettingContents.tsx

@@ -0,0 +1,52 @@
+import React, { FC } from 'react';
+
+import { useTranslation } from 'react-i18next';
+import { Card, CardBody } from 'reactstrap';
+
+import IndentForm from './IndentForm';
+import LineBreakForm from './LineBreakForm';
+import PresentationForm from './PresentationForm';
+import XssForm from './XssForm';
+
+type Props = {
+
+};
+
+
+const MarkDownSettingContents: FC<Props> = () => {
+  const { t } = useTranslation();
+
+  return (
+    <div data-testid="admin-markdown">
+      {/* Line Break Setting */}
+      <h2 className="admin-setting-header">{t('admin:markdown_setting.lineBreak_header')}</h2>
+      <Card className="card well my-3">
+        <CardBody className="px-0 py-2">{ t('admin:markdown_setting.lineBreak_desc') }</CardBody>
+      </Card>
+      <LineBreakForm />
+
+      {/* Indent Setting */}
+      <h2 className="admin-setting-header">{t('admin:markdown_setting.indent_header')}</h2>
+      <Card className="card well my-3">
+        <CardBody className="px-0 py-2">{t('admin:markdown_setting.indent_desc') }</CardBody>
+      </Card>
+      <IndentForm />
+
+      {/* Presentation Setting */}
+      <h2 className="admin-setting-header">{ t('admin:markdown_setting.presentation_header') }</h2>
+      <Card className="card well my-3">
+        <CardBody className="px-0 py-2">{ t('admin:markdown_setting.presentation_desc') }</CardBody>
+      </Card>
+      <PresentationForm />
+
+      {/* XSS Setting */}
+      <h2 className="admin-setting-header">{ t('admin:markdown_setting.xss_header') }</h2>
+      <Card className="card well my-3">
+        <CardBody className="px-0 py-2">{ t('admin:markdown_setting.xss_desc') }</CardBody>
+      </Card>
+      <XssForm />
+    </div>
+  );
+};
+
+export default MarkDownSettingContents;

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

+ 18 - 9
packages/app/src/components/Admin/Security/GitHubSecuritySettingContents.jsx

@@ -1,13 +1,15 @@
 /* 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 AdminGitHubSecurityContainer from '~/client/services/AdminGitHubSecurityContainer';
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
 
 class GitHubSecurityManagementContents extends React.Component {
 
@@ -183,6 +185,18 @@ class GitHubSecurityManagementContents extends React.Component {
 
 }
 
+const GitHubSecurityManagementContentsFC = (props) => {
+  const { t } = useTranslation();
+  return <GitHubSecurityManagementContents t={t} {...props} />;
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const GitHubSecurityManagementContentsWrapper = withUnstatedContainers(GitHubSecurityManagementContentsFC, [
+  AdminGeneralSecurityContainer,
+  AdminGitHubSecurityContainer,
+]);
 
 GitHubSecurityManagementContents.propTypes = {
   t: PropTypes.func.isRequired, // i18next
@@ -190,9 +204,4 @@ GitHubSecurityManagementContents.propTypes = {
   adminGitHubSecurityContainer: PropTypes.instanceOf(AdminGitHubSecurityContainer).isRequired,
 };
 
-const GitHubSecurityManagementContentsWrapper = withUnstatedContainers(GitHubSecurityManagementContents, [
-  AdminGeneralSecurityContainer,
-  AdminGitHubSecurityContainer,
-]);
-
-export default withTranslation()(GitHubSecurityManagementContentsWrapper);
+export default GitHubSecurityManagementContentsWrapper;

+ 8 - 6
packages/app/src/components/Admin/Security/GoogleSecuritySettingContents.jsx

@@ -2,12 +2,11 @@
 import React from 'react';
 
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 
 
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminGoogleSecurityContainer from '~/client/services/AdminGoogleSecurityContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -193,18 +192,21 @@ class GoogleSecurityManagementContents extends React.Component {
 
 }
 
+const GoogleSecurityManagementContentsFc = (props) => {
+  const { t } = useTranslation();
+  return <GoogleSecurityManagementContents t={t} {...props} />;
+};
+
 
 GoogleSecurityManagementContents.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
   adminGoogleSecurityContainer: PropTypes.instanceOf(AdminGoogleSecurityContainer).isRequired,
 };
 
-const GoogleSecurityManagementContentsWrapper = withUnstatedContainers(GoogleSecurityManagementContents, [
-  AppContainer,
+const GoogleSecurityManagementContentsWrapper = withUnstatedContainers(GoogleSecurityManagementContentsFc, [
   AdminGeneralSecurityContainer,
   AdminGoogleSecurityContainer,
 ]);
 
-export default withTranslation()(GoogleSecurityManagementContentsWrapper);
+export default GoogleSecurityManagementContentsWrapper;

+ 8 - 5
packages/app/src/components/Admin/Security/LdapAuthTest.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 AdminLdapSecurityContainer from '~/client/services/AdminLdapSecurityContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiPost } from '~/client/util/apiv1-client';
 import loggerFactory from '~/utils/logger';
@@ -129,10 +128,14 @@ class LdapAuthTest extends React.Component {
 
 }
 
+const LdapAuthTestFc = (props) => {
+  const { t } = useTranslation();
+  return <LdapAuthTest t={t} {...props} />;
+};
+
 
 LdapAuthTest.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminLdapSecurityContainer: PropTypes.instanceOf(AdminLdapSecurityContainer).isRequired,
 
   username: PropTypes.string.isRequired,
@@ -141,6 +144,6 @@ LdapAuthTest.propTypes = {
   onChangePassword: PropTypes.func.isRequired,
 };
 
-const LdapAuthTestWrapper = withUnstatedContainers(LdapAuthTest, [AppContainer, AdminLdapSecurityContainer]);
+const LdapAuthTestWrapper = withUnstatedContainers(LdapAuthTestFc, [AdminLdapSecurityContainer]);
 
-export default withTranslation()(LdapAuthTestWrapper);
+export default LdapAuthTestWrapper;

+ 3 - 10
packages/app/src/components/Admin/Security/LdapAuthTestModal.jsx

@@ -1,7 +1,6 @@
 import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
 
+import PropTypes from 'prop-types';
 import {
   Modal,
   ModalHeader,
@@ -10,8 +9,6 @@ import {
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
-import AppContainer from '~/client/services/AppContainer';
-import AdminLdapSecurityContainer from '~/client/services/AdminLdapSecurityContainer';
 import LdapAuthTest from './LdapAuthTest';
 
 
@@ -66,14 +63,10 @@ class LdapAuthTestModal extends React.Component {
 
 
 LdapAuthTestModal.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminLdapSecurityContainer: PropTypes.instanceOf(AdminLdapSecurityContainer).isRequired,
-
   isOpen: PropTypes.bool.isRequired,
   onClose: PropTypes.func.isRequired,
 };
 
-const LdapAuthTestModalWrapper = withUnstatedContainers(LdapAuthTestModal, [AppContainer, AdminLdapSecurityContainer]);
+const LdapAuthTestModalWrapper = withUnstatedContainers(LdapAuthTestModal, []);
 
-export default withTranslation()(LdapAuthTestModalWrapper);
+export default LdapAuthTestModalWrapper;

+ 13 - 9
packages/app/src/components/Admin/Security/LdapSecuritySettingContents.jsx

@@ -1,13 +1,14 @@
 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 PropTypes from 'prop-types';
+import { useTranslation } from 'react-i18next';
 
-import AppContainer from '~/client/services/AppContainer';
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminLdapSecurityContainer from '~/client/services/AdminLdapSecurityContainer';
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
+
 import LdapAuthTestModal from './LdapAuthTestModal';
 
 
@@ -432,15 +433,18 @@ class LdapSecuritySettingContents extends React.Component {
 
 LdapSecuritySettingContents.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
   adminLdapSecurityContainer: PropTypes.instanceOf(AdminLdapSecurityContainer).isRequired,
 };
 
-const LdapSecuritySettingContentsWrapper = withUnstatedContainers(LdapSecuritySettingContents, [
-  AppContainer,
+const LdapSecuritySettingContentsWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <LdapSecuritySettingContents t={t} {...props} />;
+};
+
+const LdapSecuritySettingContentsWrapper = withUnstatedContainers(LdapSecuritySettingContentsWrapperFC, [
   AdminGeneralSecurityContainer,
   AdminLdapSecurityContainer,
 ]);
 
-export default withTranslation()(LdapSecuritySettingContentsWrapper);
+export default LdapSecuritySettingContentsWrapper;

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

@@ -1,14 +1,16 @@
 /* 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 AppContainer from '~/client/services/AppContainer';
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminLocalSecurityContainer from '~/client/services/AdminLocalSecurityContainer';
+import AppContainer from '~/client/services/AppContainer';
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
 
 class LocalSecuritySettingContents extends React.Component {
 
@@ -249,10 +251,15 @@ LocalSecuritySettingContents.propTypes = {
   adminLocalSecurityContainer: PropTypes.instanceOf(AdminLocalSecurityContainer).isRequired,
 };
 
-const LocalSecuritySettingContentsWrapper = withUnstatedContainers(LocalSecuritySettingContents, [
+const LocalSecuritySettingContentsWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <LocalSecuritySettingContents t={t} {...props} />;
+};
+
+const LocalSecuritySettingContentsWrapper = withUnstatedContainers(LocalSecuritySettingContentsWrapperFC, [
   AppContainer,
   AdminGeneralSecurityContainer,
   AdminLocalSecurityContainer,
 ]);
 
-export default withTranslation()(LocalSecuritySettingContentsWrapper);
+export default LocalSecuritySettingContentsWrapper;

+ 12 - 8
packages/app/src/components/Admin/Security/OidcSecuritySettingContents.jsx

@@ -1,14 +1,15 @@
 /* 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 AppContainer from '~/client/services/AppContainer';
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminOidcSecurityContainer from '~/client/services/AdminOidcSecurityContainer';
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
 
 class OidcSecurityManagementContents extends React.Component {
 
@@ -462,15 +463,18 @@ class OidcSecurityManagementContents extends React.Component {
 
 OidcSecurityManagementContents.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
   adminOidcSecurityContainer: PropTypes.instanceOf(AdminOidcSecurityContainer).isRequired,
 };
 
-const OidcSecurityManagementContentsWrapper = withUnstatedContainers(OidcSecurityManagementContents, [
-  AppContainer,
+const OidcSecurityManagementContentsWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <OidcSecurityManagementContents t={t} {...props} />;
+};
+
+const OidcSecurityManagementContentsWrapper = withUnstatedContainers(OidcSecurityManagementContentsWrapperFC, [
   AdminGeneralSecurityContainer,
   AdminOidcSecurityContainer,
 ]);
 
-export default withTranslation()(OidcSecurityManagementContentsWrapper);
+export default OidcSecurityManagementContentsWrapper;

+ 12 - 9
packages/app/src/components/Admin/Security/SamlSecuritySettingContents.jsx

@@ -1,16 +1,16 @@
 /* eslint-disable react/no-danger */
 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 { Collapse } from 'reactstrap';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
-import AppContainer from '~/client/services/AppContainer';
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminSamlSecurityContainer from '~/client/services/AdminSamlSecurityContainer';
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
 
 class SamlSecurityManagementContents extends React.Component {
 
@@ -532,15 +532,18 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
 
 SamlSecurityManagementContents.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
   adminSamlSecurityContainer: PropTypes.instanceOf(AdminSamlSecurityContainer).isRequired,
 };
 
-const SamlSecurityManagementContentsWrapper = withUnstatedContainers(SamlSecurityManagementContents, [
-  AppContainer,
+const SamlSecurityManagementContentsWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <SamlSecurityManagementContents t={t} {...props} />;
+};
+
+const SamlSecurityManagementContentsWrapper = withUnstatedContainers(SamlSecurityManagementContentsWrapperFC, [
   AdminGeneralSecurityContainer,
   AdminSamlSecurityContainer,
 ]);
 
-export default withTranslation()(SamlSecurityManagementContentsWrapper);
+export default SamlSecurityManagementContentsWrapper;

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

+ 13 - 9
packages/app/src/components/Admin/Security/SecuritySetting.jsx

@@ -1,15 +1,16 @@
 /* eslint-disable react/no-danger */
 import React from 'react';
+
 import PropTypes from 'prop-types';
+import { useTranslation } from 'react-i18next';
 import { Collapse } from 'reactstrap';
-import { withTranslation } from 'react-i18next';
 
-import { validateDeleteConfigs, prepareDeleteConfigValuesForCalc } from '~/utils/page-delete-config';
-import { withUnstatedContainers } from '../../UnstatedUtils';
+import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { PageDeleteConfigValue } from '~/interfaces/page-delete-config';
-import AppContainer from '~/client/services/AppContainer';
-import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
+import { validateDeleteConfigs, prepareDeleteConfigValuesForCalc } from '~/utils/page-delete-config';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
 
 // used as the prefix of translation
 const DeletionTypeForT = Object.freeze({
@@ -490,11 +491,14 @@ class SecuritySetting extends React.Component {
 
 SecuritySetting.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  csrf: PropTypes.string,
   adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
 };
 
-const SecuritySettingWrapper = withUnstatedContainers(SecuritySetting, [AppContainer, AdminGeneralSecurityContainer]);
+const SecuritySettingWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <SecuritySetting t={t} {...props} />;
+};
+
+const SecuritySettingWrapper = withUnstatedContainers(SecuritySettingWrapperFC, [AdminGeneralSecurityContainer]);
 
-export default withTranslation()(SecuritySettingWrapper);
+export default SecuritySettingWrapper;

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

+ 4 - 6
packages/app/src/components/Admin/SlackIntegration/DeleteSlackBotSettingsModal.jsx

@@ -1,14 +1,13 @@
 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';
 
 const DeleteSlackBotSettingsModal = React.memo((props) => {
-  const { t } = props;
+  const { t } = useTranslation();
 
   function closeModal() {
     if (props.onClose == null) {
@@ -86,11 +85,10 @@ const DeleteSlackBotSettingsModal = React.memo((props) => {
 });
 
 DeleteSlackBotSettingsModal.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
   isResetAll: PropTypes.bool.isRequired,
   isOpen: PropTypes.bool.isRequired,
   onClose: PropTypes.func,
   onClickDeleteButton: PropTypes.func,
 };
 
-export default withTranslation()(DeleteSlackBotSettingsModal);
+export default DeleteSlackBotSettingsModal;

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

+ 23 - 25
packages/app/src/components/Admin/UserGroupDetail/RadioButtonForSerchUserOption.jsx

@@ -1,37 +1,35 @@
 
 import React from 'react';
+
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-class RadioButtonForSerchUserOption extends React.Component {
-
-  render() {
-    const { t, searchType } = this.props;
-    return (
-      <div className="custom-control custom-radio custom-control-inline" key={`${searchType}Match`}>
-        <input
-          type="radio"
-          id={`${searchType}Match`}
-          className="custom-control-input"
-          checked={this.props.checked}
-          onChange={this.props.onChange}
-        />
-        <label className="text-capitalize custom-control-label ml-3" htmlFor={`${searchType}Match`}>
-          {t(`admin:user_group_management.add_modal.${searchType}_match`)}
-        </label>
-      </div>
-    );
-  }
-
-}
+import { useTranslation } from 'react-i18next';
+
+const RadioButtonForSerchUserOption = (props) => {
+
+  const { searchType } = props;
+  const { t } = useTranslation();
+  return (
+    <div className="custom-control custom-radio custom-control-inline" key={`${searchType}Match`}>
+      <input
+        type="radio"
+        id={`${searchType}Match`}
+        className="custom-control-input"
+        checked={props.checked}
+        onChange={props.onChange}
+      />
+      <label className="text-capitalize custom-control-label ml-3" htmlFor={`${searchType}Match`}>
+        {t(`admin:user_group_management.add_modal.${searchType}_match`)}
+      </label>
+    </div>
+  );
+};
 
 
 RadioButtonForSerchUserOption.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
 
   searchType: PropTypes.string.isRequired,
   checked: PropTypes.bool.isRequired,
   onChange: PropTypes.func.isRequired,
 };
 
-export default withTranslation()(RadioButtonForSerchUserOption);
+export default RadioButtonForSerchUserOption;

+ 8 - 3
packages/app/src/components/Admin/UserGroupDetail/UserGroupPageList.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 AdminUserGroupDetailContainer from '~/client/services/AdminUserGroupDetailContainer';
 import AppContainer from '~/client/services/AppContainer';
@@ -84,9 +84,14 @@ UserGroupPageList.propTypes = {
   adminUserGroupDetailContainer: PropTypes.instanceOf(AdminUserGroupDetailContainer).isRequired,
 };
 
+const UserGroupPageListWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <UserGroupPageList t={t} {...props} />;
+};
+
 /**
  * Wrapper component for using unstated
  */
-const UserGroupPageListWrapper = withUnstatedContainers(UserGroupPageList, [AppContainer, AdminUserGroupDetailContainer]);
+const UserGroupPageListWrapper = withUnstatedContainers(UserGroupPageListWrapperFC, [AppContainer, AdminUserGroupDetailContainer]);
 
-export default withTranslation()(UserGroupPageListWrapper);
+export default UserGroupPageListWrapper;

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

@@ -3,7 +3,7 @@ import React from 'react';
 import { UserPicture } from '@growi/ui';
 import PropTypes from 'prop-types';
 import { AsyncTypeahead } from 'react-bootstrap-typeahead';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 import { debounce } from 'throttle-debounce';
 
 import AdminUserGroupDetailContainer from '~/client/services/AdminUserGroupDetailContainer';
@@ -165,9 +165,14 @@ UserGroupUserFormByInput.propTypes = {
   adminUserGroupDetailContainer: PropTypes.instanceOf(AdminUserGroupDetailContainer).isRequired,
 };
 
+const UserGroupUserFormByInputWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <UserGroupUserFormByInput t={t} {...props} />;
+};
+
 /**
  * Wrapper component for using unstated
  */
-const UserGroupUserFormByInputWrapper = withUnstatedContainers(UserGroupUserFormByInput, [AppContainer, AdminUserGroupDetailContainer]);
+const UserGroupUserFormByInputWrapper = withUnstatedContainers(UserGroupUserFormByInputWrapperFC, [AppContainer, AdminUserGroupDetailContainer]);
 
-export default withTranslation()(UserGroupUserFormByInputWrapper);
+export default UserGroupUserFormByInputWrapper;

+ 14 - 7
packages/app/src/components/Admin/UserGroupDetail/UserGroupUserModal.jsx

@@ -1,16 +1,19 @@
 import React from 'react';
+
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
 import {
   Modal, ModalHeader, ModalBody,
 } from 'reactstrap';
 
-import UserGroupUserFormByInput from './UserGroupUserFormByInput';
-import { withUnstatedContainers } from '../../UnstatedUtils';
-import AppContainer from '~/client/services/AppContainer';
 import AdminUserGroupDetailContainer from '~/client/services/AdminUserGroupDetailContainer';
-import RadioButtonForSerchUserOption from './RadioButtonForSerchUserOption';
+import AppContainer from '~/client/services/AppContainer';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
+
 import CheckBoxForSerchUserOption from './CheckBoxForSerchUserOption';
+import RadioButtonForSerchUserOption from './RadioButtonForSerchUserOption';
+import UserGroupUserFormByInput from './UserGroupUserFormByInput';
 
 class UserGroupUserModal extends React.Component {
 
@@ -82,9 +85,13 @@ UserGroupUserModal.propTypes = {
   adminUserGroupDetailContainer: PropTypes.instanceOf(AdminUserGroupDetailContainer).isRequired,
 };
 
+const UserGroupUserModalWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <UserGroupUserModal t={t} {...props} />;
+};
 /**
  * Wrapper component for using unstated
  */
-const UserGroupUserModalWrapper = withUnstatedContainers(UserGroupUserModal, [AppContainer, AdminUserGroupDetailContainer]);
+const UserGroupUserModalWrapper = withUnstatedContainers(UserGroupUserModalWrapperFC, [AppContainer, AdminUserGroupDetailContainer]);
 
-export default withTranslation()(UserGroupUserModalWrapper);
+export default UserGroupUserModalWrapper;

+ 14 - 7
packages/app/src/components/Admin/UserGroupDetail/UserGroupUserTable.jsx

@@ -1,14 +1,16 @@
 import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-import dateFnsFormat from 'date-fns/format';
 
 import { UserPicture } from '@growi/ui';
-import { withUnstatedContainers } from '../../UnstatedUtils';
-import AppContainer from '~/client/services/AppContainer';
+import dateFnsFormat from 'date-fns/format';
+import PropTypes from 'prop-types';
+import { useTranslation } from 'react-i18next';
+
 import AdminUserGroupDetailContainer from '~/client/services/AdminUserGroupDetailContainer';
+import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
+import { withUnstatedContainers } from '../../UnstatedUtils';
+
 class UserGroupUserTable extends React.Component {
 
   constructor(props) {
@@ -115,9 +117,14 @@ UserGroupUserTable.propTypes = {
   adminUserGroupDetailContainer: PropTypes.instanceOf(AdminUserGroupDetailContainer).isRequired,
 };
 
+const UserGroupUserTableWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <UserGroupUserTable t={t} {...props} />;
+};
+
 /**
  * Wrapper component for using unstated
  */
-const UserGroupUserTableWrapper = withUnstatedContainers(UserGroupUserTable, [AppContainer, AdminUserGroupDetailContainer]);
+const UserGroupUserTableWrapper = withUnstatedContainers(UserGroupUserTableWrapperFC, [AppContainer, AdminUserGroupDetailContainer]);
 
-export default withTranslation()(UserGroupUserTableWrapper);
+export default UserGroupUserTableWrapper;

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

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

@@ -1,14 +1,15 @@
 import React, { Fragment } from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
+
 import dateFnsFormat from 'date-fns/format';
+import PropTypes from 'prop-types';
+import { useTranslation } from 'react-i18next';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
-import AppContainer from '~/client/services/AppContainer';
 import AdminExternalAccountsContainer from '~/client/services/AdminExternalAccountsContainer';
-
+import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
+import { withUnstatedContainers } from '../../UnstatedUtils';
+
 class ExternalAccountTable extends React.Component {
 
   constructor(props) {
@@ -122,7 +123,12 @@ ExternalAccountTable.propTypes = {
   adminExternalAccountsContainer: PropTypes.instanceOf(AdminExternalAccountsContainer).isRequired,
 };
 
-const ExternalAccountTableWrapper = withUnstatedContainers(ExternalAccountTable, [AppContainer, AdminExternalAccountsContainer]);
+const ExternalAccountTableWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <ExternalAccountTable t={t} {...props} />;
+};
+
+const ExternalAccountTableWrapper = withUnstatedContainers(ExternalAccountTableWrapperFC, [AppContainer, AdminExternalAccountsContainer]);
 
 
-export default withTranslation()(ExternalAccountTableWrapper);
+export default ExternalAccountTableWrapper;

+ 12 - 5
packages/app/src/components/Admin/Users/GiveAdminButton.jsx

@@ -1,11 +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 AdminUsersContainer from '~/client/services/AdminUsersContainer';
 import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
-import AdminUsersContainer from '~/client/services/AdminUsersContainer';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
 
 class GiveAdminButton extends React.Component {
 
@@ -39,10 +41,15 @@ class GiveAdminButton extends React.Component {
 
 }
 
+const GiveAdminButtonWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <GiveAdminButton t={t} {...props} />;
+};
+
 /**
  * Wrapper component for using unstated
  */
-const GiveAdminButtonWrapper = withUnstatedContainers(GiveAdminButton, [AppContainer, AdminUsersContainer]);
+const GiveAdminButtonWrapper = withUnstatedContainers(GiveAdminButtonWrapperFC, [AppContainer, AdminUsersContainer]);
 
 GiveAdminButton.propTypes = {
   t: PropTypes.func.isRequired, // i18next
@@ -52,4 +59,4 @@ GiveAdminButton.propTypes = {
   user: PropTypes.object.isRequired,
 };
 
-export default withTranslation()(GiveAdminButtonWrapper);
+export default GiveAdminButtonWrapper;

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

+ 8 - 3
packages/app/src/components/Admin/Users/PasswordResetModal.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 {
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
@@ -109,10 +109,15 @@ class PasswordResetModal extends React.Component {
 
 }
 
+const PasswordResetModalWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <PasswordResetModal t={t} {...props} />;
+};
+
 /**
  * Wrapper component for using unstated
  */
-const PasswordResetModalWrapper = withUnstatedContainers(PasswordResetModal, [AppContainer]);
+const PasswordResetModalWrapper = withUnstatedContainers(PasswordResetModalWrapperFC, [AppContainer]);
 
 PasswordResetModal.propTypes = {
   t: PropTypes.func.isRequired, // i18next
@@ -124,4 +129,4 @@ PasswordResetModal.propTypes = {
 
 };
 
-export default withTranslation()(PasswordResetModalWrapper);
+export default PasswordResetModalWrapper;

+ 86 - 0
packages/app/src/components/Admin/Users/RemoveAdminButton.jsx

@@ -0,0 +1,86 @@
+import React, { Fragment } from 'react';
+
+import PropTypes from 'prop-types';
+import { useTranslation } from 'react-i18next';
+
+import AdminUsersContainer from '~/client/services/AdminUsersContainer';
+import AppContainer from '~/client/services/AppContainer';
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
+
+class RemoveAdminButton extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.onClickRemoveAdminBtn = this.onClickRemoveAdminBtn.bind(this);
+  }
+
+  async onClickRemoveAdminBtn() {
+    const { t } = this.props;
+
+    try {
+      const username = await this.props.adminUsersContainer.removeUserAdmin(this.props.user._id);
+      toastSuccess(t('toaster.remove_user_admin', { username }));
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }
+
+
+  renderRemoveAdminBtn() {
+    const { t } = this.props;
+
+    return (
+      <button className="dropdown-item" type="button" onClick={() => { this.onClickRemoveAdminBtn() }}>
+        <i className="icon-fw icon-user-unfollow"></i> {t('admin:user_management.user_table.remove_admin_access')}
+      </button>
+    );
+  }
+
+  renderRemoveAdminAlert() {
+    const { t } = this.props;
+
+    return (
+      <div className="px-4">
+        <i className="icon-fw icon-user-unfollow mb-2"></i>{t('admin:user_management.user_table.remove_admin_access')}
+        <p className="alert alert-danger">{t('admin:user_management.user_table.cannot_remove')}</p>
+      </div>
+    );
+  }
+
+  render() {
+    const { user } = this.props;
+    const { currentUsername } = this.props.appContainer;
+
+    return (
+      <Fragment>
+        {user.username !== currentUsername ? this.renderRemoveAdminBtn()
+          : this.renderRemoveAdminAlert()}
+      </Fragment>
+    );
+  }
+
+}
+
+const RemoveAdminButtonWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <RemoveAdminButton t={t} {...props} />;
+};
+
+/**
+* Wrapper component for using unstated
+*/
+const RemoveAdminButtonWrapper = withUnstatedContainers(RemoveAdminButtonWrapperFC, [AppContainer, AdminUsersContainer]);
+
+RemoveAdminButton.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminUsersContainer: PropTypes.instanceOf(AdminUsersContainer).isRequired,
+
+  user: PropTypes.object.isRequired,
+};
+
+export default RemoveAdminButtonWrapper;

+ 2 - 3
packages/app/src/components/Admin/Users/SortIcons.jsx

@@ -1,6 +1,6 @@
 import React from 'react';
+
 import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
 
 const SortIcons = (props) => {
 
@@ -28,5 +28,4 @@ SortIcons.propTypes = {
   isAsc: PropTypes.bool.isRequired,
 };
 
-
-export default withTranslation()(SortIcons);
+export default SortIcons;

+ 12 - 5
packages/app/src/components/Admin/Users/StatusActivateButton.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 StatusActivateButton extends React.Component {
 
   constructor(props) {
@@ -39,10 +41,15 @@ class StatusActivateButton extends React.Component {
 
 }
 
+const StatusActivateFormWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <StatusActivateButton t={t} {...props} />;
+};
+
 /**
  * Wrapper component for using unstated
  */
-const StatusActivateFormWrapper = withUnstatedContainers(StatusActivateButton, [AppContainer, AdminUsersContainer]);
+const StatusActivateFormWrapper = withUnstatedContainers(StatusActivateFormWrapperFC, [AppContainer, AdminUsersContainer]);
 
 StatusActivateButton.propTypes = {
   t: PropTypes.func.isRequired, // i18next
@@ -52,4 +59,4 @@ StatusActivateButton.propTypes = {
   user: PropTypes.object.isRequired,
 };
 
-export default withTranslation()(StatusActivateFormWrapper);
+export default StatusActivateFormWrapper;

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