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

Merge remote-tracking branch 'origin/master' into imprv/95886-omit-CommentContainer

Yuki Takei 3 лет назад
Родитель
Сommit
88b568fb15
100 измененных файлов с 2295 добавлено и 2144 удалено
  1. 1 1
      package.json
  2. 32 0
      packages/app/public/images/customize-settings/dock-dark.svg
  3. 32 0
      packages/app/public/images/customize-settings/dock-light.svg
  4. 31 0
      packages/app/public/images/customize-settings/drawer-dark.svg
  5. 31 0
      packages/app/public/images/customize-settings/drawer-light.svg
  6. 7 0
      packages/app/public/static/locales/en_US/admin/admin.json
  7. 7 0
      packages/app/public/static/locales/ja_JP/admin/admin.json
  8. 7 0
      packages/app/public/static/locales/zh_CN/admin/admin.json
  9. 5 12
      packages/app/src/client/app.jsx
  10. 8 0
      packages/app/src/client/base.jsx
  11. 12 6
      packages/app/src/client/services/ContextExtractor.tsx
  12. 0 7
      packages/app/src/client/services/EditorContainer.js
  13. 1 0
      packages/app/src/client/services/PageContainer.js
  14. 34 0
      packages/app/src/client/services/ShowPageAccessoriesModal.tsx
  15. 0 69
      packages/app/src/client/services/TagContainer.js
  16. 9 9
      packages/app/src/client/util/apiv1-client.ts
  17. 2 6
      packages/app/src/client/util/editor.ts
  18. 39 41
      packages/app/src/components/Admin/AdminHome/InstalledPluginTable.jsx
  19. 0 59
      packages/app/src/components/Admin/AdminHome/SystemInfomationTable.jsx
  20. 53 0
      packages/app/src/components/Admin/AdminHome/SystemInfomationTable.tsx
  21. 139 145
      packages/app/src/components/Admin/App/AppSetting.jsx
  22. 0 113
      packages/app/src/components/Admin/App/AppSettingsPageContents.jsx
  23. 108 0
      packages/app/src/components/Admin/App/AppSettingsPageContents.tsx
  24. 20 24
      packages/app/src/components/Admin/App/FileUploadSetting.tsx
  25. 10 11
      packages/app/src/components/Admin/App/GcsSettings.jsx
  26. 16 18
      packages/app/src/components/Admin/App/MailSetting.tsx
  27. 0 79
      packages/app/src/components/Admin/App/PluginSetting.jsx
  28. 66 0
      packages/app/src/components/Admin/App/PluginSetting.tsx
  29. 10 16
      packages/app/src/components/Admin/App/SesSetting.tsx
  30. 0 105
      packages/app/src/components/Admin/App/SiteUrlSetting.jsx
  31. 93 0
      packages/app/src/components/Admin/App/SiteUrlSetting.tsx
  32. 14 17
      packages/app/src/components/Admin/App/SmtpSetting.tsx
  33. 8 5
      packages/app/src/components/Admin/Common/AdminNavigation.jsx
  34. 0 23
      packages/app/src/components/Admin/Common/AdminUpdateButtonRow.jsx
  35. 23 0
      packages/app/src/components/Admin/Common/AdminUpdateButtonRow.tsx
  36. 9 12
      packages/app/src/components/Admin/Common/LabeledProgressBar.tsx
  37. 12 7
      packages/app/src/components/Admin/Customize/Customize.jsx
  38. 0 79
      packages/app/src/components/Admin/Customize/CustomizeCssSetting.jsx
  39. 68 0
      packages/app/src/components/Admin/Customize/CustomizeCssSetting.tsx
  40. 0 39
      packages/app/src/components/Admin/Customize/CustomizeFunctionOption.jsx
  41. 37 0
      packages/app/src/components/Admin/Customize/CustomizeFunctionOption.tsx
  42. 0 174
      packages/app/src/components/Admin/Customize/CustomizeFunctionSetting.jsx
  43. 163 0
      packages/app/src/components/Admin/Customize/CustomizeFunctionSetting.tsx
  44. 0 89
      packages/app/src/components/Admin/Customize/CustomizeHeaderSetting.jsx
  45. 76 0
      packages/app/src/components/Admin/Customize/CustomizeHeaderSetting.tsx
  46. 0 156
      packages/app/src/components/Admin/Customize/CustomizeHighlightSetting.jsx
  47. 145 0
      packages/app/src/components/Admin/Customize/CustomizeHighlightSetting.tsx
  48. 4 12
      packages/app/src/components/Admin/Customize/CustomizeLayoutSetting.tsx
  49. 0 120
      packages/app/src/components/Admin/Customize/CustomizeScriptSetting.jsx
  50. 107 0
      packages/app/src/components/Admin/Customize/CustomizeScriptSetting.tsx
  51. 118 0
      packages/app/src/components/Admin/Customize/CustomizeSidebarSetting.tsx
  52. 78 80
      packages/app/src/components/Admin/Customize/CustomizeThemeOptions.jsx
  53. 0 72
      packages/app/src/components/Admin/Customize/CustomizeThemeSetting.jsx
  54. 58 0
      packages/app/src/components/Admin/Customize/CustomizeThemeSetting.tsx
  55. 2 3
      packages/app/src/components/Admin/Customize/PagingSizeUncontrolledDropdown.jsx
  56. 8 3
      packages/app/src/components/Admin/ElasticsearchManagement/ElasticsearchManagement.jsx
  57. 0 47
      packages/app/src/components/Admin/ElasticsearchManagement/NormalizeIndicesControls.jsx
  58. 35 0
      packages/app/src/components/Admin/ElasticsearchManagement/NormalizeIndicesControls.tsx
  59. 10 6
      packages/app/src/components/Admin/ElasticsearchManagement/RebuildIndexControls.jsx
  60. 0 46
      packages/app/src/components/Admin/ElasticsearchManagement/ReconnectControls.jsx
  61. 36 0
      packages/app/src/components/Admin/ElasticsearchManagement/ReconnectControls.tsx
  62. 8 8
      packages/app/src/components/Admin/ElasticsearchManagement/StatusTable.jsx
  63. 0 66
      packages/app/src/components/Admin/ExportArchiveData/ArchiveFilesTable.jsx
  64. 51 0
      packages/app/src/components/Admin/ExportArchiveData/ArchiveFilesTable.tsx
  65. 0 46
      packages/app/src/components/Admin/ExportArchiveData/ArchiveFilesTableMenu.jsx
  66. 33 0
      packages/app/src/components/Admin/ExportArchiveData/ArchiveFilesTableMenu.tsx
  67. 7 9
      packages/app/src/components/Admin/ExportArchiveData/SelectCollectionsModal.jsx
  68. 0 50
      packages/app/src/components/Admin/ImportData/GrowiArchive/ErrorViewer.jsx
  69. 34 0
      packages/app/src/components/Admin/ImportData/GrowiArchive/ErrorViewer.tsx
  70. 8 8
      packages/app/src/components/Admin/ImportData/GrowiArchive/ImportCollectionConfigurationModal.jsx
  71. 1 4
      packages/app/src/components/Admin/ImportData/GrowiArchive/ImportCollectionItem.jsx
  72. 10 6
      packages/app/src/components/Admin/ImportData/GrowiArchive/ImportForm.jsx
  73. 8 2
      packages/app/src/components/Admin/ImportData/GrowiArchive/UploadForm.jsx
  74. 7 9
      packages/app/src/components/Admin/ImportData/GrowiArchiveSection.jsx
  75. 14 6
      packages/app/src/components/Admin/ImportData/ImportDataPageContents.jsx
  76. 13 8
      packages/app/src/components/Admin/LegacySlackIntegration/SlackConfiguration.jsx
  77. 17 18
      packages/app/src/components/Admin/MarkdownSetting/IndentForm.tsx
  78. 12 10
      packages/app/src/components/Admin/MarkdownSetting/LineBreakForm.jsx
  79. 0 55
      packages/app/src/components/Admin/MarkdownSetting/MarkDownSettingContents.jsx
  80. 52 0
      packages/app/src/components/Admin/MarkdownSetting/MarkDownSettingContents.tsx
  81. 14 8
      packages/app/src/components/Admin/MarkdownSetting/PresentationForm.jsx
  82. 14 6
      packages/app/src/components/Admin/MarkdownSetting/WhiteListInput.jsx
  83. 14 7
      packages/app/src/components/Admin/MarkdownSetting/XssForm.jsx
  84. 14 7
      packages/app/src/components/Admin/Notification/GlobalNotification.jsx
  85. 10 4
      packages/app/src/components/Admin/Notification/GlobalNotificationList.jsx
  86. 11 4
      packages/app/src/components/Admin/Notification/ManageGlobalNotification.jsx
  87. 10 3
      packages/app/src/components/Admin/Notification/NotificationDeleteModal.jsx
  88. 9 2
      packages/app/src/components/Admin/Notification/TriggerEventCheckBox.jsx
  89. 16 8
      packages/app/src/components/Admin/Notification/UserNotificationRow.jsx
  90. 14 7
      packages/app/src/components/Admin/Notification/UserTriggerNotification.jsx
  91. 13 6
      packages/app/src/components/Admin/Security/BasicSecuritySettingContents.jsx
  92. 10 4
      packages/app/src/components/Admin/Security/DeleteAllShareLinksModal.jsx
  93. 18 9
      packages/app/src/components/Admin/Security/GitHubSecuritySettingContents.jsx
  94. 8 6
      packages/app/src/components/Admin/Security/GoogleSecuritySettingContents.jsx
  95. 8 5
      packages/app/src/components/Admin/Security/LdapAuthTest.jsx
  96. 3 10
      packages/app/src/components/Admin/Security/LdapAuthTestModal.jsx
  97. 13 9
      packages/app/src/components/Admin/Security/LdapSecuritySettingContents.jsx
  98. 13 6
      packages/app/src/components/Admin/Security/LocalSecuritySettingContents.jsx
  99. 12 8
      packages/app/src/components/Admin/Security/OidcSecuritySettingContents.jsx
  100. 12 9
      packages/app/src/components/Admin/Security/SamlSecuritySettingContents.jsx

+ 1 - 1
package.json

@@ -83,7 +83,7 @@
     "ts-jest": "^27.0.4",
     "ts-node": "^9.1.1",
     "tsconfig-paths": "^3.9.0",
-    "typescript": "^4.2.3",
+    "typescript": "~4.6",
     "yargs": "^17.3.1"
   },
   "engines": {

Разница между файлами не показана из-за своего большого размера
+ 32 - 0
packages/app/public/images/customize-settings/dock-dark.svg


Разница между файлами не показана из-за своего большого размера
+ 32 - 0
packages/app/public/images/customize-settings/dock-light.svg


+ 31 - 0
packages/app/public/images/customize-settings/drawer-dark.svg

@@ -0,0 +1,31 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="249" height="160" viewBox="0 0 249 160">
+  <g transform="translate(17766 9529)">
+    <rect width="249" height="160" rx="2" transform="translate(-17766 -9529)" fill="#2a2d33"/>
+    <g transform="translate(-17700 -9500)">
+      <rect width="170" height="5" transform="translate(0 10)" fill="#8e9aa7" stroke="rgba(0,0,0,0)" stroke-width="1"/>
+      <rect width="42.646" height="5" fill="#8e9aa7" stroke="rgba(0,0,0,0)" stroke-width="1"/>
+      <rect width="170" height="5" transform="translate(0 20)" fill="#8e9aa7" stroke="rgba(0,0,0,0)" stroke-width="1"/>
+      <rect width="170" height="5" transform="translate(0 30)" fill="#8e9aa7" stroke="rgba(0,0,0,0)" stroke-width="1"/>
+    </g>
+    <g transform="translate(-17700 -9435)">
+      <rect width="170" height="5" transform="translate(0 10)" fill="#8e9aa7" stroke="rgba(0,0,0,0)" stroke-width="1"/>
+      <rect width="42.646" height="5" fill="#8e9aa7" stroke="rgba(0,0,0,0)" stroke-width="1"/>
+      <rect width="170" height="5" transform="translate(0 20)" fill="#8e9aa7" stroke="rgba(0,0,0,0)" stroke-width="1"/>
+      <rect width="170" height="5" transform="translate(0 30)" fill="#8e9aa7" stroke="rgba(0,0,0,0)" stroke-width="1"/>
+    </g>
+    <rect width="249" height="160" transform="translate(-17766 -9529)" fill="#16171d" opacity="0.586"/>
+    <g transform="translate(-217 -20)">
+      <path d="M2,160H-2V0H2Z" transform="translate(-17461 -9509)" fill="#209fd8"/>
+      <rect width="86" height="160" transform="translate(-17549 -9509)" fill="#343a40"/>
+    </g>
+    <g transform="translate(-217 -9)">
+      <rect width="47" height="5" transform="translate(-17530 -9431)" fill="#8e9aa7" stroke="rgba(0,0,0,0)" stroke-width="1"/>
+      <rect width="47" height="5" transform="translate(-17530 -9441)" fill="#8e9aa7" stroke="rgba(0,0,0,0)" stroke-width="1"/>
+      <rect width="47" height="5" transform="translate(-17530 -9451)" fill="#8e9aa7" stroke="rgba(0,0,0,0)" stroke-width="1"/>
+      <rect width="47" height="5" transform="translate(-17530 -9461)" fill="#8e9aa7" stroke="rgba(0,0,0,0)" stroke-width="1"/>
+      <rect width="47" height="5" transform="translate(-17530 -9471)" fill="#8e9aa7" stroke="rgba(0,0,0,0)" stroke-width="1"/>
+      <rect width="47" height="5" transform="translate(-17530 -9481)" fill="#8e9aa7" stroke="rgba(0,0,0,0)" stroke-width="1"/>
+      <rect width="11.787" height="5" transform="translate(-17530 -9491)" fill="#8e9aa7" stroke="rgba(0,0,0,0)" stroke-width="1"/>
+    </g>
+  </g>
+</svg>

+ 31 - 0
packages/app/public/images/customize-settings/drawer-light.svg

@@ -0,0 +1,31 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="249" height="160" viewBox="0 0 249 160">
+  <g transform="translate(17766 9529)">
+    <rect width="249" height="160" rx="2" transform="translate(-17766 -9529)" fill="#fff"/>
+    <g transform="translate(-17700 -9500)">
+      <rect width="170" height="5" transform="translate(0 10)" fill="#abb4bd"/>
+      <rect width="42.646" height="5" fill="#abb4bd"/>
+      <rect width="170" height="5" transform="translate(0 20)" fill="#abb4bd"/>
+      <rect width="170" height="5" transform="translate(0 30)" fill="#abb4bd"/>
+    </g>
+    <g transform="translate(-17700 -9435)">
+      <rect width="170" height="5" transform="translate(0 10)" fill="#abb4bd"/>
+      <rect width="42.646" height="5" fill="#abb4bd"/>
+      <rect width="170" height="5" transform="translate(0 20)" fill="#abb4bd"/>
+      <rect width="170" height="5" transform="translate(0 30)" fill="#abb4bd"/>
+    </g>
+    <rect width="249" height="160" transform="translate(-17766 -9529)" fill="#25272f" opacity="0.271"/>
+    <g transform="translate(-217 -20)">
+      <path d="M2,160H-2V0H2Z" transform="translate(-17461 -9509)" fill="#209fd8"/>
+      <rect width="86" height="160" transform="translate(-17549 -9509)" fill="#f3f7fc"/>
+    </g>
+    <g transform="translate(-217 -9)">
+      <rect width="47" height="5" transform="translate(-17530 -9431)" fill="#abb4bd"/>
+      <rect width="47" height="5" transform="translate(-17530 -9441)" fill="#abb4bd"/>
+      <rect width="47" height="5" transform="translate(-17530 -9451)" fill="#abb4bd"/>
+      <rect width="47" height="5" transform="translate(-17530 -9461)" fill="#abb4bd"/>
+      <rect width="47" height="5" transform="translate(-17530 -9471)" fill="#abb4bd"/>
+      <rect width="47" height="5" transform="translate(-17530 -9481)" fill="#abb4bd"/>
+      <rect width="11.787" height="5" transform="translate(-17530 -9491)" fill="#abb4bd"/>
+    </g>
+  </g>
+</svg>

+ 7 - 0
packages/app/public/static/locales/en_US/admin/admin.json

@@ -149,6 +149,13 @@
     }
   },
   "customize_setting": {
+    "default_sidebar_mode": {
+      "title": "Default sidebar mode",
+      "desc": "You can set the sidebar mode for new users and guests visiting the page.",
+      "dock_mode_default_desc": "You can set the initial state of the sidebar when Dock Mode is selected.",
+      "dock_mode_default_open": "Open the page as it was opened from the beginning",
+      "dock_mode_default_close": "Open the page as it was closed from the beginning"
+    },
     "layout": "Layout",
     "layout_options": {
       "default": "Default content width",

+ 7 - 0
packages/app/public/static/locales/ja_JP/admin/admin.json

@@ -149,6 +149,13 @@
     }
   },
   "customize_setting": {
+    "default_sidebar_mode": {
+      "title": "デフォルトのサイドバーモード",
+      "desc": "新規ユーザー、ページを訪れたゲストのサイドバーモードを設定できます。",
+      "dock_mode_default_desc": "Dock Mode選択時のサイドバーの初期状態を設定できます。",
+      "dock_mode_default_open": "初めから開いた状態でページを開く",
+      "dock_mode_default_close": "初めから閉じた状態でページを開く"
+    },
     "layout": "レイアウト",
     "layout_options": {
       "default": "デフォルトのコンテンツ幅",

+ 7 - 0
packages/app/public/static/locales/zh_CN/admin/admin.json

@@ -148,6 +148,13 @@
     }
   },
   "customize_setting": {
+    "default_sidebar_mode": {
+      "title": "默认的侧边栏模式",
+      "desc": "你可以为新用户和访问该网页的客人设置侧边栏模式。",
+      "dock_mode_default_desc": "当选择Dock模式时,可以设置侧边栏的初始状态。",
+      "dock_mode_default_open": "从头开始翻页",
+      "dock_mode_default_close": "从头开始打开关闭的页面"
+    },
     "layout": "布局",
     "layout_options": {
       "default": "默认内容宽度 ",

+ 5 - 12
packages/app/src/client/app.jsx

@@ -13,7 +13,6 @@ import PageContainer from '~/client/services/PageContainer';
 import PageHistoryContainer from '~/client/services/PageHistoryContainer';
 import PersonalContainer from '~/client/services/PersonalContainer';
 import RevisionComparerContainer from '~/client/services/RevisionComparerContainer';
-import TagContainer from '~/client/services/TagContainer';
 import IdenticalPathPage from '~/components/IdenticalPathPage';
 import PrivateLegacyPages from '~/components/PrivateLegacyPages';
 import loggerFactory from '~/utils/logger';
@@ -49,8 +48,6 @@ import TagPage from '../components/TagPage';
 import TrashPageList from '../components/TrashPageList';
 
 import { appContainer, componentMappings } from './base';
-import { toastError } from './util/apiNotification';
-
 
 const logger = loggerFactory('growi:cli:app');
 
@@ -64,11 +61,10 @@ const pageContainer = new PageContainer(appContainer);
 const pageHistoryContainer = new PageHistoryContainer(appContainer, pageContainer);
 const revisionComparerContainer = new RevisionComparerContainer(appContainer, pageContainer);
 const editorContainer = new EditorContainer(appContainer);
-const tagContainer = new TagContainer(appContainer);
 const personalContainer = new PersonalContainer(appContainer);
 const injectableContainers = [
   appContainer, socketIoContainer, pageContainer, pageHistoryContainer, revisionComparerContainer,
-  editorContainer, tagContainer, personalContainer,
+  editorContainer, personalContainer,
 ];
 
 logger.info('unstated containers have been initialized');
@@ -96,8 +92,6 @@ Object.assign(componentMappings, {
 
   'trash-page-alert': <TrashPageAlert />,
 
-  'fix-page-grant-alert': <FixPageGrantAlert />,
-
   'trash-page-list-container': <TrashPageList />,
 
   'not-found-page': <NotFoundPage />,
@@ -130,11 +124,10 @@ if (pageContainer.state.pageId != null) {
 
     'recent-created-icon': <RecentlyCreatedIcon />,
   });
-
-  // show the Page accessory modal when query of "compare" is requested
-  if (revisionComparerContainer.getRevisionIDsToCompareAsParam().length > 0) {
-    toastError('Sorry, opening PageAccessoriesModal is not implemented yet in v5.');
-  //   pageAccessoriesContainer.openPageAccessoriesModal('pageHistory');
+  if (!pageContainer.state.isEmpty) {
+    Object.assign(componentMappings, {
+      'fix-page-grant-alert': <FixPageGrantAlert />,
+    });
   }
 }
 if (pageContainer.state.creator != null) {

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

@@ -6,6 +6,8 @@ import AppContainer from '~/client/services/AppContainer';
 import SocketIoContainer from '~/client/services/SocketIoContainer';
 import { DescendantsPageListModal } from '~/components/DescendantsPageListModal';
 import PutbackPageModal from '~/components/PutbackPageModal';
+import ShortcutsModal from '~/components/ShortcutsModal';
+import SystemVersion from '~/components/SystemVersion';
 import InterceptorManager from '~/services/interceptor-manager';
 import Xss from '~/services/xss';
 import loggerFactory from '~/utils/logger';
@@ -21,6 +23,8 @@ import PageDuplicateModal from '../components/PageDuplicateModal';
 import PagePresentationModal from '../components/PagePresentationModal';
 import PageRenameModal from '../components/PageRenameModal';
 
+import ShowPageAccessoriesModal from './services/ShowPageAccessoriesModal';
+
 const logger = loggerFactory('growi:cli:app');
 
 if (!window) {
@@ -61,9 +65,13 @@ const componentMappings = {
   'page-accessories-modal': <PageAccessoriesModal />,
   'descendants-page-list-modal': <DescendantsPageListModal />,
   'page-put-back-modal': <PutbackPageModal />,
+  'shortcuts-modal': <ShortcutsModal />,
 
   'grw-hotkeys-manager': <HotkeysManager />,
+  'system-version': <SystemVersion />,
+
 
+  'show-page-accessories-modal': <ShowPageAccessoriesModal />,
 };
 
 export { appContainer, componentMappings };

+ 12 - 6
packages/app/src/client/services/ContextExtractor.tsx

@@ -18,7 +18,7 @@ import {
   useShareLinkId, useShareLinksNumber, useTemplateTagData, useCurrentUpdatedAt, useCreator, useRevisionAuthor, useCurrentUser, useTargetAndAncestors,
   useNotFoundTargetPathOrId, useIsSearchPage, useIsForbidden, useIsIdenticalPath, useHasParent,
   useIsAclEnabled, useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsEnabledAttachTitleHeader, useIsNotFoundPermalink,
-  useDefaultIndentSize, useIsIndentSizeForced, useCsrfToken,
+  useDefaultIndentSize, useIsIndentSizeForced, useCsrfToken, useIsEmptyPage, useEmptyPageId, useGrowiVersion,
 } from '../../stores/context';
 
 const { isTrashPage: _isTrashPage } = pagePathUtils;
@@ -29,7 +29,7 @@ const ContextExtractorOnce: FC = () => {
 
   const mainContent = document.querySelector('#content-main');
   const notFoundContentForPt = document.getElementById('growi-pagetree-not-found-context');
-  const notFoundContent = document.getElementById('growi-not-found-context');
+  const notFoundContext = document.getElementById('growi-not-found-context');
   const forbiddenContent = document.getElementById('forbidden-page');
 
   // get csrf token from body element
@@ -57,7 +57,10 @@ const ContextExtractorOnce: FC = () => {
    */
   const revisionId = mainContent?.getAttribute('data-page-revision-id');
   const path = decodeURI(mainContent?.getAttribute('data-path') || '');
+  // assign `null` to avoid returning empty string
   const pageId = mainContent?.getAttribute('data-page-id') || null;
+  const emptyPageId = notFoundContext?.getAttribute('data-page-id') || null;
+
   const revisionCreatedAt = +(mainContent?.getAttribute('data-page-revision-created') || '');
 
   // createdAt
@@ -89,8 +92,9 @@ const ContextExtractorOnce: FC = () => {
   const revisionAuthor = JSON.parse(mainContent?.getAttribute('data-page-revision-author') || jsonNull);
   const targetAndAncestors = JSON.parse(document.getElementById('growi-pagetree-target-and-ancestors')?.textContent || jsonNull);
   const notFoundTargetPathOrId = JSON.parse(notFoundContentForPt?.getAttribute('data-not-found-target-path-or-id') || jsonNull);
-  const isNotFoundPermalink = JSON.parse(notFoundContent?.getAttribute('data-is-not-found-permalink') || jsonNull);
+  const isNotFoundPermalink = JSON.parse(notFoundContext?.getAttribute('data-is-not-found-permalink') || jsonNull);
   const isSearchPage = document.getElementById('search-page') != null;
+  const isEmptyPage = JSON.parse(mainContent?.getAttribute('data-page-is-empty') || jsonNull) ?? false;
 
   const grant = +(mainContent?.getAttribute('data-page-grant') || 1);
   const grantGroupId = mainContent?.getAttribute('data-page-grant-group') || null;
@@ -105,9 +109,9 @@ const ContextExtractorOnce: FC = () => {
   useCurrentUser(currentUser);
 
   // UserUISettings
-  usePreferDrawerModeByUser(userUISettings?.preferDrawerModeByUser);
+  usePreferDrawerModeByUser(userUISettings?.preferDrawerModeByUser ?? configByContextHydrate.isSidebarDrawerMode);
   usePreferDrawerModeOnEditByUser(userUISettings?.preferDrawerModeOnEditByUser);
-  useSidebarCollapsed(userUISettings?.isSidebarCollapsed);
+  useSidebarCollapsed(userUISettings?.isSidebarCollapsed ?? configByContextHydrate.isSidebarClosedAtDockMode);
   useCurrentSidebarContents(userUISettings?.currentSidebarContents);
   useCurrentProductNavWidth(userUISettings?.currentProductNavWidth);
 
@@ -119,7 +123,7 @@ const ContextExtractorOnce: FC = () => {
   useIsEnabledAttachTitleHeader(configByContextHydrate.isEnabledAttachTitleHeader);
   useIsIndentSizeForced(configByContextHydrate.isIndentSizeForced);
   useDefaultIndentSize(configByContextHydrate.adminPreferredIndentSize);
-
+  useGrowiVersion(configByContextHydrate.crowi.version);
 
   // Page
   useCurrentCreatedAt(createdAt);
@@ -135,6 +139,7 @@ const ContextExtractorOnce: FC = () => {
   useIsUserPage(isUserPage);
   useLastUpdateUsername(lastUpdateUsername);
   useCurrentPageId(pageId);
+  useEmptyPageId(emptyPageId);
   usePageIdOnHackmd(pageIdOnHackmd);
   usePageUser(pageUser);
   useCurrentPagePath(path);
@@ -151,6 +156,7 @@ const ContextExtractorOnce: FC = () => {
   useNotFoundTargetPathOrId(notFoundTargetPathOrId);
   useIsNotFoundPermalink(isNotFoundPermalink);
   useIsSearchPage(isSearchPage);
+  useIsEmptyPage(isEmptyPage);
   useHasParent(hasParent);
 
   // Navigation

+ 0 - 7
packages/app/src/client/services/EditorContainer.js

@@ -59,13 +59,6 @@ export default class EditorContainer extends Container {
     }
   }
 
-  getCurrentOptionsToSave() {
-    const opt = {
-      pageTags: this.state.tags,
-    };
-
-    return opt;
-  }
 
   // See https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload#example
   showUnsavedWarning(e) {

+ 1 - 0
packages/app/src/client/services/PageContainer.js

@@ -52,6 +52,7 @@ export default class PageContainer extends Container {
       revisionId,
       revisionCreatedAt: +mainContent.getAttribute('data-page-revision-created'),
       path,
+      isEmpty: mainContent.getAttribute('data-page-is-empty'),
 
       createdAt: mainContent.getAttribute('data-page-created-at'),
       // please use useCurrentUpdatedAt instead

+ 34 - 0
packages/app/src/client/services/ShowPageAccessoriesModal.tsx

@@ -0,0 +1,34 @@
+import React, { useEffect, useState } from 'react';
+
+import { usePageAccessoriesModal, PageAccessoriesModalContents } from '~/stores/modal';
+
+function getURLQueryParamValue(key: string) {
+// window.location.href is page URL;
+  const queryStr: URLSearchParams = new URL(window.location.href).searchParams;
+  return queryStr.get(key);
+}
+
+const queryCompareFormat = new RegExp(/([a-z0-9]){24}...([a-z0-9]){24}/);
+
+const ShowPageAccessoriesModal = (): JSX.Element => {
+  const { data: status, open: openPageAccessories } = usePageAccessoriesModal();
+  const [isArleadyMounted, setIsArleadyMounted] = useState(false);
+  useEffect(() => {
+    const pageIdParams = getURLQueryParamValue('compare');
+    if (status == null || status.isOpened === true) {
+      return;
+    }
+    if (isArleadyMounted === true) {
+      return;
+    }
+    if (pageIdParams != null) {
+      if (queryCompareFormat.test(pageIdParams)) {
+        openPageAccessories(PageAccessoriesModalContents.PageHistory);
+      }
+    }
+    setIsArleadyMounted(true);
+  }, [openPageAccessories, status, isArleadyMounted]);
+  return <></>;
+};
+
+export default ShowPageAccessoriesModal;

+ 0 - 69
packages/app/src/client/services/TagContainer.js

@@ -1,69 +0,0 @@
-import { Container } from 'unstated';
-
-import loggerFactory from '~/utils/logger';
-
-import { apiGet } from '../util/apiv1-client';
-
-const logger = loggerFactory('growi:services:TagContainer');
-
-/**
- * Service container related to Tag
- * @extends {Container} unstated Container
- */
-export default class TagContainer extends Container {
-
-  constructor(appContainer) {
-    super();
-
-    this.appContainer = appContainer;
-    this.appContainer.registerContainer(this);
-
-    this.init();
-  }
-
-  /**
-   * Workaround for the mangling in production build to break constructor.name
-   */
-  static getClassName() {
-    return 'TagContainer';
-  }
-
-  /**
-   * retrieve tags data
-   * !! This method should be invoked after PageContainer and EditorContainer has been initialized !!
-   */
-  async init() {
-    const pageContainer = this.appContainer.getContainer('PageContainer');
-    const editorContainer = this.appContainer.getContainer('EditorContainer');
-
-    if (Object.keys(pageContainer.state).length === 0) {
-      logger.debug('There is no need to initialize TagContainer because this is not a Page');
-      return;
-    }
-
-    const { pageId, templateTagData, shareLinkId } = pageContainer.state;
-
-    if (shareLinkId != null) {
-      return;
-    }
-
-    let tags = [];
-    // when the page exists or shared page
-    if (pageId != null && shareLinkId == null) {
-      const res = await apiGet('/pages.getPageTag', { pageId });
-      tags = res.tags;
-    }
-    // when the page not exist
-    else if (templateTagData != null) {
-      tags = templateTagData.split(',').filter((str) => {
-        return str !== ''; // filter empty values
-      });
-    }
-
-    logger.debug('tags data has been initialized');
-
-    pageContainer.setState({ tags });
-    editorContainer.setState({ tags });
-  }
-
-}

+ 9 - 9
packages/app/src/client/util/apiv1-client.ts

@@ -30,7 +30,7 @@ class Apiv1ErrorHandler extends Error {
 
 }
 
-export async function apiRequest(method: string, path: string, params: unknown): Promise<unknown> {
+export async function apiRequest<T>(method: string, path: string, params: unknown): Promise<T> {
   const res = await axios[method](urljoin(apiv1Root, path), params);
 
   if (res.data.ok) {
@@ -46,29 +46,29 @@ export async function apiRequest(method: string, path: string, params: unknown):
   throw new Error(res.data.error);
 }
 
-export async function apiGet(path: string, params: unknown = {}): Promise<unknown> {
-  return apiRequest('get', path, { params });
+export async function apiGet<T>(path: string, params: unknown = {}): Promise<T> {
+  return apiRequest<T>('get', path, { params });
 }
 
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
-export async function apiPost(path: string, params: any & ParamWithCsrfKey = {}): Promise<unknown> {
+export async function apiPost<T>(path: string, params: any & ParamWithCsrfKey = {}): Promise<T> {
   if (params._csrf == null) {
     params._csrf = csrfToken;
   }
-  return apiRequest('post', path, params);
+  return apiRequest<T>('post', path, params);
 }
 
-export async function apiPostForm(path: string, formData: FormData): Promise<unknown> {
+export async function apiPostForm<T>(path: string, formData: FormData): Promise<T> {
   if (formData.get('_csrf') == null && csrfToken != null) {
     formData.append('_csrf', csrfToken);
   }
-  return apiPost(path, formData);
+  return apiPost<T>(path, formData);
 }
 
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
-export async function apiDelete(path: string, params: any & ParamWithCsrfKey = {}): Promise<unknown> {
+export async function apiDelete<T>(path: string, params: any & ParamWithCsrfKey = {}): Promise<T> {
   if (params._csrf == null) {
     params._csrf = csrfToken;
   }
-  return apiRequest('delete', path, { data: params });
+  return apiRequest<T>('delete', path, { data: params });
 }

+ 2 - 6
packages/app/src/client/util/editor.ts

@@ -1,5 +1,3 @@
-import EditorContainer from '~/client/services/EditorContainer';
-
 type OptionsToSave = {
   isSlackEnabled: boolean;
   slackChannels: string;
@@ -9,18 +7,16 @@ type OptionsToSave = {
   grantUserGroupName?: string | null;
 };
 
-// TODO: Remove editorContainer upon migration to SWR
 export const getOptionsToSave = (
     isSlackEnabled: boolean,
     slackChannels: string,
     grant: number,
     grantUserGroupId: string | null | undefined,
     grantUserGroupName: string | null | undefined,
-    editorContainer: EditorContainer,
+    pageTags: string[],
 ): OptionsToSave => {
-  const optionsToSave = editorContainer.getCurrentOptionsToSave();
   return {
-    ...optionsToSave,
+    pageTags,
     isSlackEnabled,
     slackChannels,
     grant,

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

+ 12 - 7
packages/app/src/components/Admin/Customize/Customize.jsx

@@ -1,23 +1,25 @@
 
 import React, { Fragment } from 'react';
+
 import PropTypes from 'prop-types';
 
-import loggerFactory from '~/utils/logger';
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
 import AppContainer from '~/client/services/AppContainer';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastError } from '~/client/util/apiNotification';
 import { toArrayIfNot } from '~/utils/array-utils';
+import loggerFactory from '~/utils/logger';
+
 import { withLoadingSppiner } from '../../SuspenseUtils';
+import { withUnstatedContainers } from '../../UnstatedUtils';
 
-import CustomizeLayoutSetting from './CustomizeLayoutSetting';
-import CustomizeThemeSetting from './CustomizeThemeSetting';
+import CustomizeCssSetting from './CustomizeCssSetting';
 import CustomizeFunctionSetting from './CustomizeFunctionSetting';
+import CustomizeHeaderSetting from './CustomizeHeaderSetting';
 import CustomizeHighlightSetting from './CustomizeHighlightSetting';
-import CustomizeCssSetting from './CustomizeCssSetting';
+import CustomizeLayoutSetting from './CustomizeLayoutSetting';
 import CustomizeScriptSetting from './CustomizeScriptSetting';
-import CustomizeHeaderSetting from './CustomizeHeaderSetting';
+import CustomizeSidebarSetting from './CustomizeSidebarSetting';
+import CustomizeThemeSetting from './CustomizeThemeSetting';
 import CustomizeTitle from './CustomizeTitle';
 
 const logger = loggerFactory('growi:services:AdminCustomizePage');
@@ -53,6 +55,9 @@ function Customize(props) {
       <div className="mb-5">
         <CustomizeThemeSetting />
       </div>
+      <div className="mb-5">
+        <CustomizeSidebarSetting />
+      </div>
       <div className="mb-5">
         <CustomizeFunctionSetting />
       </div>

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

+ 118 - 0
packages/app/src/components/Admin/Customize/CustomizeSidebarSetting.tsx

@@ -0,0 +1,118 @@
+import React, { useCallback } from 'react';
+
+import { useTranslation } from 'react-i18next';
+import { Card, CardBody } from 'reactstrap';
+
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { isDarkMode as isDarkModeByUtil } from '~/client/util/color-scheme';
+import { useSWRxSidebarConfig } from '~/stores/ui';
+
+const CustomizeSidebarsetting = (): JSX.Element => {
+  const { t } = useTranslation();
+  const {
+    update, isSidebarDrawerMode, isSidebarClosedAtDockMode, setIsSidebarDrawerMode, setIsSidebarClosedAtDockMode,
+  } = useSWRxSidebarConfig();
+
+  const isDarkMode = isDarkModeByUtil();
+  const colorText = isDarkMode ? 'dark' : 'light';
+  const drawerIconFileName = `/images/customize-settings/drawer-${colorText}.svg`;
+  const dockIconFileName = `/images/customize-settings/dock-${colorText}.svg`;
+
+  const onClickSubmit = useCallback(async() => {
+    try {
+      await update();
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.default_sidebar_mode.title') }));
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }, [t, update]);
+
+  return (
+    <React.Fragment>
+      <div className="row">
+        <div className="col-12">
+
+          <h2 className="admin-setting-header">{t('admin:customize_setting.default_sidebar_mode.title')}</h2>
+
+          <Card className="card well my-3">
+            <CardBody className="px-0 py-2">
+              {t('admin:customize_setting.default_sidebar_mode.desc')}
+            </CardBody>
+          </Card>
+
+          <div className="d-flex justify-content-around mt-5">
+            <div id="layoutOptions" className="card-deck">
+              <div
+                className={`card customize-layout-card ${isSidebarDrawerMode ? 'border-active' : ''}`}
+                onClick={() => setIsSidebarDrawerMode(true)}
+                role="button"
+              >
+                <img src={drawerIconFileName} />
+                <div className="card-body text-center">
+                  Drawer Mode
+                </div>
+              </div>
+              <div
+                className={`card customize-layout-card ${!isSidebarDrawerMode ? 'border-active' : ''}`}
+                onClick={() => setIsSidebarDrawerMode(false)}
+                role="button"
+              >
+                <img src={dockIconFileName} />
+                <div className="card-body  text-center">
+                  Dock Mode
+                </div>
+              </div>
+            </div>
+          </div>
+
+          <Card className="card well my-5">
+            <CardBody className="px-0 py-2">
+              {t('admin:customize_setting.default_sidebar_mode.dock_mode_default_desc')}
+            </CardBody>
+          </Card>
+
+          <div className="px-3">
+            <div className="custom-control custom-radio my-3">
+              <input
+                type="radio"
+                id="is-open"
+                className="custom-control-input"
+                name="mailVisibility"
+                checked={!isSidebarDrawerMode && !isSidebarClosedAtDockMode}
+                disabled={isSidebarDrawerMode}
+                onChange={() => setIsSidebarClosedAtDockMode(false)}
+              />
+              <label className="custom-control-label" htmlFor="is-open">
+                {t('admin:customize_setting.default_sidebar_mode.dock_mode_default_open')}
+              </label>
+            </div>
+            <div className="custom-control custom-radio my-3">
+              <input
+                type="radio"
+                id="is-closed"
+                className="custom-control-input"
+                name="mailVisibility"
+                checked={!isSidebarDrawerMode && isSidebarClosedAtDockMode}
+                disabled={isSidebarDrawerMode}
+                onChange={() => setIsSidebarClosedAtDockMode(true)}
+              />
+              <label className="custom-control-label" htmlFor="is-closed">
+                {t('admin:customize_setting.default_sidebar_mode.dock_mode_default_close')}
+              </label>
+            </div>
+          </div>
+
+          <div className="row my-3">
+            <div className="mx-auto">
+              <button type="button" onClick={onClickSubmit} className="btn btn-primary">{ t('Update') }</button>
+            </div>
+          </div>
+
+        </div>
+      </div>
+    </React.Fragment>
+  );
+};
+
+export default CustomizeSidebarsetting;

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

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