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

Merge branch 'master' into feat/96430-sidebar-default-mode

Taichi Masuyama 3 лет назад
Родитель
Сommit
86320c7b97
100 измененных файлов с 2007 добавлено и 2251 удалено
  1. 66 1
      CHANGELOG.md
  2. 1 1
      lerna.json
  3. 2 2
      package.json
  4. 1 0
      packages/app/config/webpack.common.js
  5. 4 4
      packages/app/docker/README.md
  6. 11 11
      packages/app/package.json
  7. 0 0
      packages/app/public/static/locales/en_US/admin/admin.json
  8. 0 0
      packages/app/public/static/locales/en_US/meta.json
  9. 13 1
      packages/app/public/static/locales/en_US/translation.json
  10. 0 0
      packages/app/public/static/locales/index.js
  11. 0 0
      packages/app/public/static/locales/ja_JP/admin/admin.json
  12. 0 0
      packages/app/public/static/locales/ja_JP/meta.json
  13. 13 1
      packages/app/public/static/locales/ja_JP/translation.json
  14. 0 0
      packages/app/public/static/locales/zh_CN/admin/admin.json
  15. 0 0
      packages/app/public/static/locales/zh_CN/meta.json
  16. 15 3
      packages/app/public/static/locales/zh_CN/translation.json
  17. 36 40
      packages/app/src/client/admin.jsx
  18. 1 3
      packages/app/src/client/app.jsx
  19. 7 0
      packages/app/src/client/base.jsx
  20. 60 0
      packages/app/src/client/installer.jsx
  21. 2 1
      packages/app/src/client/legacy/crowi.js
  22. 67 65
      packages/app/src/client/nologin.jsx
  23. 4 32
      packages/app/src/client/services/AppContainer.js
  24. 2 7
      packages/app/src/client/services/CommentContainer.js
  25. 12 6
      packages/app/src/client/services/ContextExtractor.tsx
  26. 0 7
      packages/app/src/client/services/EditorContainer.js
  27. 5 26
      packages/app/src/client/services/PageContainer.js
  28. 0 77
      packages/app/src/client/services/PersonalContainer.js
  29. 0 69
      packages/app/src/client/services/TagContainer.js
  30. 9 1
      packages/app/src/client/services/page-operation.ts
  31. 14 16
      packages/app/src/client/util/GrowiRenderer.js
  32. 14 7
      packages/app/src/client/util/apiv1-client.ts
  33. 12 3
      packages/app/src/client/util/apiv3-client.ts
  34. 2 6
      packages/app/src/client/util/editor.ts
  35. 3 2
      packages/app/src/client/util/i18n.js
  36. 0 4
      packages/app/src/client/util/markdown-it/footernote.js
  37. 1 2
      packages/app/src/client/util/markdown-it/header-line-number.js
  38. 0 4
      packages/app/src/client/util/markdown-it/header-with-edit-link.js
  39. 1 3
      packages/app/src/client/util/markdown-it/header.js
  40. 0 4
      packages/app/src/client/util/markdown-it/table-with-handsontable-button.js
  41. 0 4
      packages/app/src/client/util/markdown-it/table.js
  42. 2 1
      packages/app/src/client/util/markdown-it/toc-and-anchor.js
  43. 2 2
      packages/app/src/client/util/reveal/plugins/growi-renderer.js
  44. 39 41
      packages/app/src/components/Admin/AdminHome/InstalledPluginTable.jsx
  45. 0 59
      packages/app/src/components/Admin/AdminHome/SystemInfomationTable.jsx
  46. 53 0
      packages/app/src/components/Admin/AdminHome/SystemInfomationTable.tsx
  47. 139 145
      packages/app/src/components/Admin/App/AppSetting.jsx
  48. 0 113
      packages/app/src/components/Admin/App/AppSettingsPageContents.jsx
  49. 108 0
      packages/app/src/components/Admin/App/AppSettingsPageContents.tsx
  50. 8 8
      packages/app/src/components/Admin/App/AwsSetting.jsx
  51. 20 24
      packages/app/src/components/Admin/App/FileUploadSetting.tsx
  52. 10 11
      packages/app/src/components/Admin/App/GcsSettings.jsx
  53. 16 18
      packages/app/src/components/Admin/App/MailSetting.tsx
  54. 0 79
      packages/app/src/components/Admin/App/PluginSetting.jsx
  55. 66 0
      packages/app/src/components/Admin/App/PluginSetting.tsx
  56. 10 16
      packages/app/src/components/Admin/App/SesSetting.tsx
  57. 0 105
      packages/app/src/components/Admin/App/SiteUrlSetting.jsx
  58. 93 0
      packages/app/src/components/Admin/App/SiteUrlSetting.tsx
  59. 14 17
      packages/app/src/components/Admin/App/SmtpSetting.tsx
  60. 8 5
      packages/app/src/components/Admin/Common/AdminNavigation.jsx
  61. 0 23
      packages/app/src/components/Admin/Common/AdminUpdateButtonRow.jsx
  62. 23 0
      packages/app/src/components/Admin/Common/AdminUpdateButtonRow.tsx
  63. 9 12
      packages/app/src/components/Admin/Common/LabeledProgressBar.tsx
  64. 0 79
      packages/app/src/components/Admin/Customize/CustomizeCssSetting.jsx
  65. 68 0
      packages/app/src/components/Admin/Customize/CustomizeCssSetting.tsx
  66. 0 39
      packages/app/src/components/Admin/Customize/CustomizeFunctionOption.jsx
  67. 37 0
      packages/app/src/components/Admin/Customize/CustomizeFunctionOption.tsx
  68. 0 174
      packages/app/src/components/Admin/Customize/CustomizeFunctionSetting.jsx
  69. 163 0
      packages/app/src/components/Admin/Customize/CustomizeFunctionSetting.tsx
  70. 0 89
      packages/app/src/components/Admin/Customize/CustomizeHeaderSetting.jsx
  71. 76 0
      packages/app/src/components/Admin/Customize/CustomizeHeaderSetting.tsx
  72. 0 156
      packages/app/src/components/Admin/Customize/CustomizeHighlightSetting.jsx
  73. 145 0
      packages/app/src/components/Admin/Customize/CustomizeHighlightSetting.tsx
  74. 4 12
      packages/app/src/components/Admin/Customize/CustomizeLayoutSetting.tsx
  75. 0 120
      packages/app/src/components/Admin/Customize/CustomizeScriptSetting.jsx
  76. 107 0
      packages/app/src/components/Admin/Customize/CustomizeScriptSetting.tsx
  77. 78 80
      packages/app/src/components/Admin/Customize/CustomizeThemeOptions.jsx
  78. 0 72
      packages/app/src/components/Admin/Customize/CustomizeThemeSetting.jsx
  79. 58 0
      packages/app/src/components/Admin/Customize/CustomizeThemeSetting.tsx
  80. 2 3
      packages/app/src/components/Admin/Customize/PagingSizeUncontrolledDropdown.jsx
  81. 8 3
      packages/app/src/components/Admin/ElasticsearchManagement/ElasticsearchManagement.jsx
  82. 0 47
      packages/app/src/components/Admin/ElasticsearchManagement/NormalizeIndicesControls.jsx
  83. 35 0
      packages/app/src/components/Admin/ElasticsearchManagement/NormalizeIndicesControls.tsx
  84. 10 6
      packages/app/src/components/Admin/ElasticsearchManagement/RebuildIndexControls.jsx
  85. 0 46
      packages/app/src/components/Admin/ElasticsearchManagement/ReconnectControls.jsx
  86. 36 0
      packages/app/src/components/Admin/ElasticsearchManagement/ReconnectControls.tsx
  87. 8 8
      packages/app/src/components/Admin/ElasticsearchManagement/StatusTable.jsx
  88. 0 66
      packages/app/src/components/Admin/ExportArchiveData/ArchiveFilesTable.jsx
  89. 51 0
      packages/app/src/components/Admin/ExportArchiveData/ArchiveFilesTable.tsx
  90. 0 46
      packages/app/src/components/Admin/ExportArchiveData/ArchiveFilesTableMenu.jsx
  91. 33 0
      packages/app/src/components/Admin/ExportArchiveData/ArchiveFilesTableMenu.tsx
  92. 7 9
      packages/app/src/components/Admin/ExportArchiveData/SelectCollectionsModal.jsx
  93. 8 3
      packages/app/src/components/Admin/ExportArchiveDataPage.jsx
  94. 0 33
      packages/app/src/components/Admin/FullTextSearchManagement.jsx
  95. 22 0
      packages/app/src/components/Admin/FullTextSearchManagement.tsx
  96. 0 50
      packages/app/src/components/Admin/ImportData/GrowiArchive/ErrorViewer.jsx
  97. 34 0
      packages/app/src/components/Admin/ImportData/GrowiArchive/ErrorViewer.tsx
  98. 8 8
      packages/app/src/components/Admin/ImportData/GrowiArchive/ImportCollectionConfigurationModal.jsx
  99. 1 4
      packages/app/src/components/Admin/ImportData/GrowiArchive/ImportCollectionItem.jsx
  100. 10 6
      packages/app/src/components/Admin/ImportData/GrowiArchive/ImportForm.jsx

+ 66 - 1
CHANGELOG.md

@@ -1,9 +1,61 @@
 # Changelog
 
-## [Unreleased](https://github.com/weseek/growi/compare/v5.0.7...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v5.0.9...HEAD)
 
 *Please do not manually update this file. We've automated the process.*
 
+## [v5.0.9](https://github.com/weseek/growi/compare/v5.0.8...v5.0.9) - 2022-06-13
+
+### 🚀 Improvement
+
+- imprv: Render MathJax in Preview tab of comment (#6025) @yuki-takei
+- imprv: Exception handling for user authentication (#6019) @kaoritokashiki
+- imprv: Sidebar background color on light theme and add shadow on dark theme (#6012) @shukmos
+- imprv: Limit display of notification paths (#5991) @jam411
+
+### 🐛 Bug Fixes
+
+- fix: Getting page API is broken (#6023) @yuki-takei
+- fix: MathJax does not working (#6020) @yuki-takei
+
+## [v5.0.8](https://github.com/weseek/growi/compare/v5.0.7...v5.0.8) - 2022-06-07
+
+### 🚀 Improvement
+
+- imprv: Fix subnavigation spacing (#5995) @yuki-takei
+- imprv: Set Content-Length header in response of attachment (#5972) @hiroki-hgs
+- imprv: Fix sidebar tag layout (#5984) @jam411
+- imprv: PageStatusAlert labels when data is outdated (#5961) @yuki-takei
+- imprv: Delete NotFoundAlert from not found page (#5919) @Shunm634-source
+
+### 🐛 Bug Fixes
+
+- fix: Too many footstamps icons are shown by lsx output 3 (#6000) @yuki-takei
+- fix: Adjust PageItemControl alignment (#5994) @yuki-takei
+- fix: CodeMirror placeholder color (#5993) @yuki-takei
+- fix: Chinese notation is broken on create new page modal (#5973) @jam411
+- fix: Document timestamps does not updated (#5979) @yuki-takei
+- fix: Slack channels are not automatically filled after setting up user trigger notification on v5.0.x (#5911) @kaoritokashiki
+- fix: Login required when viewing sharelink page (#5959) @yuki-takei
+- fix: Editor scroll sync by Preview scrolling does not work (#5949) @yuki-takei
+
+### 🧰 Maintenance
+
+- support: Enable garbage collection at runtime with expose-gc package (#5986) @yuki-takei
+- support: Upgrade aws-sdk to v3 (#5863) @mudana-grune
+
+## [v4.5.22](https://github.com/weseek/growi/compare/v4.5.21...v4.5.22) - 2022-06-07
+
+### 🐛 Bug Fixes
+
+- fix: Fixed the bug of auto-filling unintended values into the Email field of the User settings (#5885) @Shunm634-source
+- fix: google-oauth2 Automatically bind external accounts does not work (#5891) @kaoritokashiki
+- fix: Slack channels are not automatically filled after setting up user trigger notification (#5976) @kaoritokashiki
+
+### 🧰 Maintenance
+
+- support: Enable garbage collection at runtime with expose-gc package (#5998) @kaoritokashiki
+
 ## [v5.0.7](https://github.com/weseek/growi/compare/v5.0.6...v5.0.7) - 2022-05-30
 
 ### 💎 Features
@@ -43,6 +95,13 @@
 - fix: Can not toggle textlint function on v5.0.x (#5854) @kaoritokashiki
 - fix(google-oauth2): Automatically bind external accounts  does not work on v5.0.x (#5886) @kaoritokashiki
 
+## [v4.5.21](https://github.com/weseek/growi/compare/v4.5.20...v4.5.21) - 2022-05-23
+
+### 🐛 Bug Fixes
+
+- fix: Can not toggle textlint function on v4.5.x (https://github.com/weseek/growi/pull/5855) @kaoritokashiki
+- fix: Error on searching (https://github.com/weseek/growi/pull/5873) @miya
+
 ## [v5.0.5](https://github.com/weseek/growi/compare/v5.0.4...v5.0.5) - 2022-05-16
 
 ### 💎 Features
@@ -67,6 +126,12 @@
 
 - support: Typescriptize tag model (#5778) @kaoritokashiki
 
+## [v4.5.20](https://github.com/weseek/growi/compare/v4.5.19...v4.5.20) - 2022-05-12
+
+### 🐛 Bug Fixes
+
+- fix: Guest user cannot access share link pages (#5819) @kaoritokashiki
+
 ## [v5.0.4](https://github.com/weseek/growi/compare/v5.0.3...v5.0.4) - 2022-04-28
 
 ### 💎 Features

+ 1 - 1
lerna.json

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

+ 2 - 2
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "5.0.8-RC.0",
+  "version": "5.0.10-RC.0",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -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": {

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

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

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

@@ -10,10 +10,10 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 
-* [`5.0.7`, `5.0`, `5`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.7/docker/Dockerfile)
-* [`5.0.7-nocdn`, `5.0-nocdn`, `5-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.7/docker/Dockerfile)
-* [`4.5.15`, `4.5`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.15/docker/Dockerfile)
-* [`4.5.15-nocdn`, `4.5-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.15/docker/Dockerfile)
+* [`5.0.9`, `5.0`, `5`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.9/docker/Dockerfile)
+* [`5.0.9-nocdn`, `5.0-nocdn`, `5-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.9/docker/Dockerfile)
+* [`4.5.22`, `4.5`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.22/docker/Dockerfile)
+* [`4.5.22-nocdn`, `4.5-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.22/docker/Dockerfile)
 * [`4.4.13`, `4.4` (Dockerfile)](https://github.com/weseek/growi/blob/v4.4.13/docker/Dockerfile)
 * [`4.4.13-nocdn`, `4.4-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.4.13/docker/Dockerfile)
 

+ 11 - 11
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "5.0.8-RC.0",
+  "version": "5.0.10-RC.0",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -11,7 +11,7 @@
     "clean": "npx -y shx rm -rf dist transpiled",
     "prebuild": "yarn cross-env NODE_ENV=production run-p clean resources:*",
     "postbuild": "npx -y shx mv transpiled/src dist && npx -y shx cp -r src/server/views dist/server/ && npx -y shx rm -rf transpiled",
-    "server": "yarn cross-env NODE_ENV=production node -r dotenv-flow/config --expose_gc dist/server/app.js",
+    "server": "yarn cross-env NODE_ENV=production node -r dotenv-flow/config dist/server/app.js",
     "server:ci": "yarn server --ci",
     "preserver": "yarn cross-env NODE_ENV=production yarn migrate",
     "migrate": "node -r dotenv-flow/config node_modules/.bin/migrate-mongo up",
@@ -19,7 +19,7 @@
     "dev": "run-p dev:client dev:server",
     "dev:client": "yarn cross-env NODE_ENV=development webpack --config config/webpack.dev.js --progress --watch",
     "dev:client:nowatch": "yarn cross-env NODE_ENV=development webpack --config config/webpack.dev.js",
-    "dev:server": "yarn cross-env NODE_ENV=development ts-node-dev --inspect --expose-gc -r tsconfig-paths/register -r dotenv-flow/config --transpile-only src/server/app.ts",
+    "dev:server": "yarn cross-env NODE_ENV=development ts-node-dev --inspect -r tsconfig-paths/register -r dotenv-flow/config --transpile-only src/server/app.ts",
     "predev:client": "yarn cross-env NODE_ENV=development run-p resources:*",
     "predev:server": "yarn cross-env NODE_ENV=development yarn dev:migrate:up",
     "dev:migrate-mongo": "yarn cross-env NODE_ENV=development yarn ts-node node_modules/.bin/migrate-mongo",
@@ -64,11 +64,11 @@
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^5.0.8-RC.0",
-    "@growi/plugin-attachment-refs": "^5.0.8-RC.0",
-    "@growi/plugin-lsx": "^5.0.8-RC.0",
-    "@growi/plugin-pukiwiki-like-linker": "^5.0.8-RC.0",
-    "@growi/slack": "^5.0.8-RC.0",
+    "@growi/codemirror-textlint": "^5.0.10-RC.0",
+    "@growi/plugin-attachment-refs": "^5.0.10-RC.0",
+    "@growi/plugin-lsx": "^5.0.10-RC.0",
+    "@growi/plugin-pukiwiki-like-linker": "^5.0.10-RC.0",
+    "@growi/slack": "^5.0.10-RC.0",
     "@promster/express": "^7.0.2",
     "@promster/server": "^7.0.4",
     "@slack/events-api": "^3.0.0",
@@ -99,6 +99,7 @@
     "esa-node": "^0.2.2",
     "escape-string-regexp": "=4.0.0",
     "eslint-plugin-regex": "^1.8.0",
+    "expose-gc": "^1.0.0",
     "express": "^4.16.1",
     "express-bunyan-logger": "^1.3.3",
     "express-mongo-sanitize": "^2.1.0",
@@ -145,7 +146,6 @@
     "react-dnd-html5-backend": "^14.1.0",
     "react-image-crop": "^8.3.0",
     "react-multiline-clamp": "^2.0.0",
-    "react-tagcloud": "^2.1.1",
     "reconnecting-websocket": "^4.4.0",
     "redis": "^3.0.2",
     "rimraf": "^3.0.0",
@@ -169,7 +169,7 @@
   },
   "devDependencies": {
     "@alienfast/i18next-loader": "^1.1.4",
-    "@growi/ui": "^5.0.8-RC.0",
+    "@growi/ui": "^5.0.10-RC.0",
     "@handsontable/react": "=2.1.0",
     "@types/compression": "^1.7.0",
     "@types/express": "^4.17.11",
@@ -190,7 +190,6 @@
     "diff2html": "^3.1.2",
     "eazy-logger": "^3.1.0",
     "emoji-mart": "npm:panta82-emoji-mart@^3.0.1",
-    "markdown-it-emoji-mart": "^0.1.1",
     "eslint-plugin-cypress": "^2.12.1",
     "eslint-plugin-regex": "^1.8.0",
     "file-loader": "^5.0.2",
@@ -208,6 +207,7 @@
     "markdown-it-blockdiag": "^1.1.1",
     "markdown-it-drawio-viewer": "^1.3.1",
     "markdown-it-emoji": "^1.4.0",
+    "markdown-it-emoji-mart": "^0.1.1",
     "markdown-it-footnote": "^3.0.1",
     "markdown-it-mathjax": "^2.0.0",
     "markdown-it-named-headers": "^0.0.4",

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


+ 0 - 0
packages/app/resource/locales/en_US/meta.json → packages/app/public/static/locales/en_US/meta.json


+ 13 - 1
packages/app/resource/locales/en_US/translation.json → packages/app/public/static/locales/en_US/translation.json

@@ -5,6 +5,7 @@
   "Delete": "Delete",
   "delete_all": "Delete all",
   "Duplicate": "Duplicate",
+  "PathRecovery": "Path recovery",
   "Copy": "Copy",
   "preview":"Preview",
   "desktop":"Desktop",
@@ -147,6 +148,7 @@
   "Shareable link": "Shareable link",
   "The whitelist of registration permission E-mail address": "The whitelist of registration permission E-mail address",
   "Add tags for this page": "Add tags for this page",
+  "tag_list": "Tag list",
   "popular_tags": "Popular tags",
   "Check All tags": "check all tags",
   "You have no tag, You can set tags on pages": "You have no tag, You can set tags on pages",
@@ -983,6 +985,7 @@
     "application_already_installed": "Application already installed.",
     "email_address_could_not_be_used": "This email address could not be used. (Make sure the allowed email address)",
     "user_id_is_not_available":"This User ID is not available.",
+    "username_should_not_be_null":"Username should not be null. Please check Authentication Mechanism Settings on admin page",
     "email_address_is_already_registered":"This email address is already registered.",
     "can_not_register_maximum_number_of_users":"Can not register more than the maximum number of users.",
     "failed_to_register":"Failed to register.",
@@ -1109,6 +1112,15 @@
     "cancel_bookmark": "Cancel Bookmark",
     "receive_notifications": "Receive Notifications",
     "stop_notification": "Stop Notification",
-    "footprints": "Footprints"
+    "footprints": "Footprints",
+    "operation": {
+      "attention": {
+        "rename": "Renaming paths of descendant pages was not successful, please open the menu from the 3-point reader and select 'Path recovery'"
+      }
+    }
+  },
+  "page_operation":{
+    "paths_recovered": "Paths recovered successfully",
+    "path_recovery_failed":"Path recovery failed"
   }
 }

+ 0 - 0
packages/app/resource/locales/index.js → packages/app/public/static/locales/index.js


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


+ 0 - 0
packages/app/resource/locales/ja_JP/meta.json → packages/app/public/static/locales/ja_JP/meta.json


+ 13 - 1
packages/app/resource/locales/ja_JP/translation.json → packages/app/public/static/locales/ja_JP/translation.json

@@ -5,6 +5,7 @@
   "Delete": "削除",
   "delete_all": "全て削除",
   "Duplicate": "複製",
+  "PathRecovery": "パスを修復",
   "Copy": "コピー",
   "preview":"プレビュー",
   "desktop":"パソコン",
@@ -146,6 +147,7 @@
   "Shareable link": "このページの共有用URL",
   "The whitelist of registration permission E-mail address": "登録許可メールアドレスの<br>ホワイトリスト",
   "Add tags for this page": "タグを付ける",
+  "tag_list": "タグ一覧",
   "popular_tags": "人気のタグ",
   "Check All tags": "全てのタグを見る",
   "You have no tag, You can set tags on pages": "使用中のタグがありません",
@@ -976,6 +978,7 @@
     "application_already_installed": "アプリケーションのインストールが完了しました。",
     "email_address_could_not_be_used":"このメールアドレスは使用できません。(許可されたメールアドレスを確認してください。)",
     "user_id_is_not_available":"このユーザーIDは使用できません。",
+    "username_should_not_be_null":"Username が null になっています 管理画面の認証機構設定にて設定の確認をしてください",
     "email_address_is_already_registered":"このメールアドレスは既に登録されています。",
     "can_not_register_maximum_number_of_users":"ユーザー数が上限を超えたため登録できません。",
     "failed_to_register":"登録に失敗しました。",
@@ -1102,6 +1105,15 @@
     "cancel_bookmark": "ブックマークを取り消す",
     "receive_notifications": "通知を受け取る",
     "stop_notification": "通知を止める",
-    "footprints": "足跡"
+    "footprints": "足跡",
+    "operation": {
+      "attention": {
+        "rename": "配下のページパスの更新が正常に行われませんでした。3点リーダーからメニューを開き、「パスを修復」を選択してしてください。"
+      }
+    }
+  },
+  "page_operation":{
+    "paths_recovered": "パスを修復しました",
+    "path_recovery_failed":"パスを修復できませんでした"
   }
 }

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


+ 0 - 0
packages/app/resource/locales/zh_CN/meta.json → packages/app/public/static/locales/zh_CN/meta.json


+ 15 - 3
packages/app/resource/locales/zh_CN/translation.json → packages/app/public/static/locales/zh_CN/translation.json

@@ -5,6 +5,7 @@
 	"Delete": "删除",
 	"delete_all": "删除所有",
 	"Duplicate": "复制",
+  "PathRecovery": "路径恢复",
 	"Copy": "复制",
   "preview":"预览",
   "desktop":"电脑",
@@ -155,6 +156,7 @@
 	"Shareable link": "可分享链接",
 	"The whitelist of registration permission E-mail address": "注册许可电子邮件地址的白名单",
 	"Add tags for this page": "添加标签",
+  "tag_list": "标签列表",
   "popular_tags": "流行标签",
   "Check All tags": "检查所有标签",
 	"You have no tag, You can set tags on pages": "你没有标签,可以在页面上设置标签",
@@ -524,7 +526,7 @@
 	"template": {
 		"modal_label": {
 			"Create/Edit Template Page": "创建/编辑模板页",
-			"Create template under": "在下面创建模板页:<br/><code><small>%s</small></code>"
+			"Create template under": "在下面创建模板页"
 		},
 		"option_label": {
 			"create/edit": "创建/编辑模板页。",
@@ -985,7 +987,8 @@
 		"aws_sttings_required": "使用此功能所需的AWS设置。请询问管理员。",
 		"application_already_installed": "应用程序已安装。",
 		"email_address_could_not_be_used": "无法使用此电子邮件地址。(确保允许的电子邮件地址)",
-		"user_id_is_not_available": "此用户ID不可用。",
+    "user_id_is_not_available": "此用户ID不可用。",
+    "username_should_not_be_null":"用户名不应为空。请检查管理页面上的身份验证机制设置",
 		"email_address_is_already_registered": "此电子邮件地址已注册。",
 		"can_not_register_maximum_number_of_users": "注册的用户数不能超过最大值。",
 		"failed_to_register": "注册失败。",
@@ -1112,6 +1115,15 @@
     "cancel_bookmark": "取消书签",
     "receive_notifications": "接收通知",
     "stop_notification": "停止通知",
-    "footprints": "脚印"
+    "footprints": "脚印",
+    "operation": {
+      "attention": {
+        "rename": "重命名子孙页的路径没有成功,请从三点式阅读器上打开菜单,选择 '路径恢复'。"
+      }
+    }
+  },
+  "page_operation":{
+    "paths_recovered": "成功恢复了页面路径",
+    "path_recovery_failed":"路径恢复失败"
   }
 }

+ 36 - 40
packages/app/src/client/admin.jsx

@@ -1,55 +1,52 @@
 import React from 'react';
+
 import ReactDOM from 'react-dom';
-import { Provider } from 'unstated';
 import { I18nextProvider } from 'react-i18next';
-
 import { SWRConfig } from 'swr';
+import { Provider } from 'unstated';
 
-import loggerFactory from '~/utils/logger';
-import { swrGlobalConfiguration } from '~/utils/swr-utils';
-
-import ErrorBoundary from '../components/ErrorBoudary';
-
-import AdminHome from '../components/Admin/AdminHome/AdminHome';
-import UserGroupDetailPage from '../components/Admin/UserGroupDetail/UserGroupDetailPage';
-import NotificationSetting from '../components/Admin/Notification/NotificationSetting';
-import LegacySlackIntegration from '../components/Admin/LegacySlackIntegration/LegacySlackIntegration';
-import SlackIntegration from '../components/Admin/SlackIntegration/SlackIntegration';
-import ManageGlobalNotification from '../components/Admin/Notification/ManageGlobalNotification';
-import MarkdownSetting from '../components/Admin/MarkdownSetting/MarkDownSetting';
-import UserManagement from '../components/Admin/UserManagement';
-import AppSettingsPage from '../components/Admin/App/AppSettingsPage';
-import SecurityManagement from '../components/Admin/Security/SecurityManagement';
-import ManageExternalAccount from '../components/Admin/ManageExternalAccount';
-import UserGroupPage from '../components/Admin/UserGroup/UserGroupPage';
-import Customize from '../components/Admin/Customize/Customize';
-import ImportDataPage from '../components/Admin/ImportDataPage';
-import ExportArchiveDataPage from '../components/Admin/ExportArchiveDataPage';
-import FullTextSearchManagement from '../components/Admin/FullTextSearchManagement';
-import AdminNavigation from '../components/Admin/Common/AdminNavigation';
-
-import AdminSocketIoContainer from '~/client/services/AdminSocketIoContainer';
-import AdminHomeContainer from '~/client/services/AdminHomeContainer';
-import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
-import AdminUserGroupDetailContainer from '~/client/services/AdminUserGroupDetailContainer';
-import AdminUsersContainer from '~/client/services/AdminUsersContainer';
 import AdminAppContainer from '~/client/services/AdminAppContainer';
-import AdminImportContainer from '~/client/services/AdminImportContainer';
-import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
+import AdminBasicSecurityContainer from '~/client/services/AdminBasicSecurityContainer';
+import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
 import AdminExternalAccountsContainer from '~/client/services/AdminExternalAccountsContainer';
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
+import AdminGitHubSecurityContainer from '~/client/services/AdminGitHubSecurityContainer';
+import AdminGoogleSecurityContainer from '~/client/services/AdminGoogleSecurityContainer';
+import AdminHomeContainer from '~/client/services/AdminHomeContainer';
+import AdminImportContainer from '~/client/services/AdminImportContainer';
 import AdminLdapSecurityContainer from '~/client/services/AdminLdapSecurityContainer';
 import AdminLocalSecurityContainer from '~/client/services/AdminLocalSecurityContainer';
-import AdminSamlSecurityContainer from '~/client/services/AdminSamlSecurityContainer';
-import AdminOidcSecurityContainer from '~/client/services/AdminOidcSecurityContainer';
-import AdminBasicSecurityContainer from '~/client/services/AdminBasicSecurityContainer';
-import AdminGoogleSecurityContainer from '~/client/services/AdminGoogleSecurityContainer';
-import AdminGitHubSecurityContainer from '~/client/services/AdminGitHubSecurityContainer';
-import AdminTwitterSecurityContainer from '~/client/services/AdminTwitterSecurityContainer';
+import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
+import AdminOidcSecurityContainer from '~/client/services/AdminOidcSecurityContainer';
+import AdminSamlSecurityContainer from '~/client/services/AdminSamlSecurityContainer';
 import AdminSlackIntegrationLegacyContainer from '~/client/services/AdminSlackIntegrationLegacyContainer';
-
+import AdminSocketIoContainer from '~/client/services/AdminSocketIoContainer';
+import AdminTwitterSecurityContainer from '~/client/services/AdminTwitterSecurityContainer';
+import AdminUserGroupDetailContainer from '~/client/services/AdminUserGroupDetailContainer';
+import AdminUsersContainer from '~/client/services/AdminUsersContainer';
 import ContextExtractor from '~/client/services/ContextExtractor';
+import loggerFactory from '~/utils/logger';
+import { swrGlobalConfiguration } from '~/utils/swr-utils';
+
+import AdminHome from '../components/Admin/AdminHome/AdminHome';
+import AppSettingsPage from '../components/Admin/App/AppSettingsPage';
+import AdminNavigation from '../components/Admin/Common/AdminNavigation';
+import Customize from '../components/Admin/Customize/Customize';
+import ExportArchiveDataPage from '../components/Admin/ExportArchiveDataPage';
+import FullTextSearchManagement from '../components/Admin/FullTextSearchManagement';
+import ImportDataPage from '../components/Admin/ImportDataPage';
+import LegacySlackIntegration from '../components/Admin/LegacySlackIntegration/LegacySlackIntegration';
+import ManageExternalAccount from '../components/Admin/ManageExternalAccount';
+import MarkdownSetting from '../components/Admin/MarkdownSetting/MarkDownSetting';
+import ManageGlobalNotification from '../components/Admin/Notification/ManageGlobalNotification';
+import NotificationSetting from '../components/Admin/Notification/NotificationSetting';
+import SecurityManagement from '../components/Admin/Security/SecurityManagement';
+import SlackIntegration from '../components/Admin/SlackIntegration/SlackIntegration';
+import UserGroupPage from '../components/Admin/UserGroup/UserGroupPage';
+import UserGroupDetailPage from '../components/Admin/UserGroupDetail/UserGroupDetailPage';
+import UserManagement from '../components/Admin/UserManagement';
+import ErrorBoundary from '../components/ErrorBoudary';
 
 import { appContainer, componentMappings } from './base';
 
@@ -58,7 +55,6 @@ const logger = loggerFactory('growi:admin');
 appContainer.initContents();
 
 const { i18n } = appContainer;
-
 // create unstated container instance
 const adminAppContainer = new AdminAppContainer(appContainer);
 const adminImportContainer = new AdminImportContainer(appContainer);

+ 1 - 3
packages/app/src/client/app.jsx

@@ -14,7 +14,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';
@@ -66,11 +65,10 @@ const pageHistoryContainer = new PageHistoryContainer(appContainer, pageContaine
 const revisionComparerContainer = new RevisionComparerContainer(appContainer, pageContainer);
 const commentContainer = new CommentContainer(appContainer);
 const editorContainer = new EditorContainer(appContainer);
-const tagContainer = new TagContainer(appContainer);
 const personalContainer = new PersonalContainer(appContainer);
 const injectableContainers = [
   appContainer, socketIoContainer, pageContainer, pageHistoryContainer, revisionComparerContainer,
-  commentContainer, editorContainer, tagContainer, personalContainer,
+  commentContainer, editorContainer, personalContainer,
 ];
 
 logger.info('unstated containers have been initialized');

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

@@ -6,6 +6,9 @@ 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';
 
@@ -31,6 +34,7 @@ const xss = new Xss();
 window.xss = xss;
 
 window.globalEmitter = new EventEmitter();
+window.interceptorManager = new InterceptorManager();
 
 // create unstated container instance
 const appContainer = new AppContainer();
@@ -59,8 +63,11 @@ 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 />,
+
 
 };
 

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

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

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

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

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

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

+ 4 - 32
packages/app/src/client/services/AppContainer.js

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

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

@@ -2,7 +2,7 @@ import { Container } from 'unstated';
 
 import loggerFactory from '~/utils/logger';
 
-import { apiGet, apiPost } from '../util/apiv1-client';
+import { apiGet, apiPost, apiPostForm } from '../util/apiv1-client';
 import { apiv3Put } from '../util/apiv3-client';
 
 const logger = loggerFactory('growi:services:CommentContainer');
@@ -30,10 +30,6 @@ export default class CommentContainer extends Container {
 
     this.state = {
       comments: [],
-
-      // settings shared among all of CommentEditor
-      isSlackEnabled: false,
-      slackChannels: mainContent.getAttribute('data-slack-channels') || '',
     };
 
     this.retrieveComments = this.retrieveComments.bind(this);
@@ -161,12 +157,11 @@ export default class CommentContainer extends Container {
 
     const endpoint = '/attachments.add';
     const formData = new FormData();
-    formData.append('_csrf', this.appContainer.csrfToken);
     formData.append('file', file);
     formData.append('path', pagePath);
     formData.append('page_id', pageId);
 
-    return apiPost(endpoint, formData);
+    return apiPostForm(endpoint, formData);
   }
 
 }

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

@@ -16,9 +16,9 @@ import {
   useIsDeleted, useIsNotCreatable, useIsTrashPage, useIsUserPage, useLastUpdateUsername,
   useCurrentPageId, usePageIdOnHackmd, usePageUser, useCurrentPagePath, useRevisionCreatedAt, useRevisionId, useRevisionIdHackmdSynced,
   useShareLinkId, useShareLinksNumber, useTemplateTagData, useCurrentUpdatedAt, useCreator, useRevisionAuthor, useCurrentUser, useTargetAndAncestors,
-  useSlackChannels, useNotFoundTargetPathOrId, useIsSearchPage, useIsForbidden, useIsIdenticalPath, useHasParent,
+  useNotFoundTargetPathOrId, useIsSearchPage, useIsForbidden, useIsIdenticalPath, useHasParent,
   useIsAclEnabled, useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsEnabledAttachTitleHeader, useIsNotFoundPermalink,
-  useDefaultIndentSize, useIsIndentSizeForced,
+  useDefaultIndentSize, useIsIndentSizeForced, useCsrfToken, useGrowiVersion,
 } from '../../stores/context';
 
 const { isTrashPage: _isTrashPage } = pagePathUtils;
@@ -32,6 +32,11 @@ const ContextExtractorOnce: FC = () => {
   const notFoundContent = document.getElementById('growi-not-found-context');
   const forbiddenContent = document.getElementById('forbidden-page');
 
+  // get csrf token from body element
+  // DO NOT REMOVE: uploading attachment data requires appContainer.csrfToken
+  const body = document.querySelector('body');
+  const csrfToken = body?.dataset.csrftoken;
+
   /*
    * App Context from DOM
    */
@@ -85,7 +90,6 @@ const ContextExtractorOnce: FC = () => {
   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 slackChannels = mainContent?.getAttribute('data-slack-channels') || '';
   const isSearchPage = document.getElementById('search-page') != null;
 
   const grant = +(mainContent?.getAttribute('data-page-grant') || 1);
@@ -95,6 +99,8 @@ const ContextExtractorOnce: FC = () => {
   /*
    * use static swr
    */
+  useCsrfToken(csrfToken);
+
   // App
   useCurrentUser(currentUser);
 
@@ -113,7 +119,7 @@ const ContextExtractorOnce: FC = () => {
   useIsEnabledAttachTitleHeader(configByContextHydrate.isEnabledAttachTitleHeader);
   useIsIndentSizeForced(configByContextHydrate.isIndentSizeForced);
   useDefaultIndentSize(configByContextHydrate.adminPreferredIndentSize);
-
+  useGrowiVersion(configByContextHydrate.crowi.version);
 
   // Page
   useCurrentCreatedAt(createdAt);
@@ -158,7 +164,6 @@ const ContextExtractorOnce: FC = () => {
   useIsDeviceSmallerThanMd();
 
   // Editor
-  useSlackChannels(slackChannels);
   useSelectedGrant(grant);
   useSelectedGrantGroupId(grantGroupId);
   useSelectedGrantGroupName(grantGroupName);
@@ -168,7 +173,8 @@ const ContextExtractorOnce: FC = () => {
 
   // Global Socket
   useSetupGlobalSocket();
-  useSetupGlobalAdminSocket();
+  const shouldInitAdminSock = !!currentUser?.isAdmin;
+  useSetupGlobalAdminSocket(shouldInitAdminSock);
 
   return null;
 };

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

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

@@ -99,7 +99,7 @@ export default class PageContainer extends Container {
       logger.warn('The data of \'data-page-revision-author\' is invalid', e);
     }
 
-    const { interceptorManager } = this.appContainer;
+    const { interceptorManager } = window;
     interceptorManager.addInterceptor(new DetachCodeBlockInterceptor(), 10); // process as soon as possible
     interceptorManager.addInterceptor(new DrawioInterceptor(), 20);
     interceptorManager.addInterceptor(new RestoreCodeBlockInterceptor(), 900); // process as late as possible
@@ -136,29 +136,6 @@ export default class PageContainer extends Container {
     return 'PageContainer';
   }
 
-  /**
-   * whether to Empty Trash Page
-   * not displayed when guest user and not on trash page
-   */
-  get isAbleToShowEmptyTrashButton() {
-    const { currentUser } = this.appContainer;
-    const { path, hasChildren } = this.state;
-
-    return (currentUser != null && currentUser.admin && path === '/trash' && hasChildren);
-  }
-
-  /**
-   * whether to display trash management buttons
-   * ex.) undo, delete completly
-   * not displayed when guest user
-   */
-  get isAbleToShowTrashPageManagementButtons() {
-    const { currentUser } = this.appContainer;
-    const { isDeleted } = this.state;
-
-    return (isDeleted && currentUser != null);
-  }
-
   /**
    * initialize state for markdown data
    */
@@ -219,7 +196,8 @@ export default class PageContainer extends Container {
 
     // Update PageEditor component
     if (editorMode !== EditorMode.Editor) {
-      window.globalEmitter.emit('updateEditorValue', newState.markdown);
+      // eslint-disable-next-line no-undef
+      globalEmitter.emit('updateEditorValue', newState.markdown);
     }
 
     // PageEditorByHackmd component
@@ -459,7 +437,8 @@ export default class PageContainer extends Container {
 
     // Update PageEditor component
     if (editorMode !== EditorMode.Editor) {
-      window.globalEmitter.emit('updateEditorValue', markdown);
+      // eslint-disable-next-line no-undef
+      globalEmitter.emit('updateEditorValue', markdown);
     }
 
     editorContainer.setState({ tags: res.tags });

+ 0 - 77
packages/app/src/client/services/PersonalContainer.js

@@ -8,8 +8,6 @@ import { apiv3Get, apiv3Put } from '../util/apiv3-client';
 // eslint-disable-next-line no-unused-vars
 const logger = loggerFactory('growi:services:PersonalContainer');
 
-const DEFAULT_IMAGE = '/images/icons/user.svg';
-
 /**
  * Service container for personal settings page (PersonalSettings.jsx)
  * @extends {Container} unstated Container
@@ -29,8 +27,6 @@ export default class PersonalContainer extends Container {
       isEmailPublished: false,
       lang: 'en_US',
       isGravatarEnabled: false,
-      isUploadedPicture: false,
-      uploadedPictureSrc: this.getUploadedPictureSrc(this.appContainer.currentUser),
       externalAccounts: [],
       apiToken: '',
       slackMemberId: '',
@@ -69,25 +65,6 @@ export default class PersonalContainer extends Container {
     }
   }
 
-  /**
-   * define a function for uploaded picture
-   */
-  getUploadedPictureSrc(user) {
-    if (user == null) {
-      return DEFAULT_IMAGE;
-    }
-    if (user.image) {
-      this.setState({ isUploadedPicture: true });
-      return user.image;
-    }
-    if (user.imageAttachment != null) {
-      this.setState({ isUploadedPicture: true });
-      return user.imageAttachment.filePathProxied;
-    }
-
-    return DEFAULT_IMAGE;
-  }
-
   /**
    * retrieve external accounts that linked me
    */
@@ -178,60 +155,6 @@ export default class PersonalContainer extends Container {
     }
   }
 
-  /**
-   * Update profile image
-   * @memberOf PersonalContainer
-   */
-  async updateProfileImage() {
-    try {
-      const response = await apiv3Put('/personal-setting/image-type', {
-        isGravatarEnabled: this.state.isGravatarEnabled,
-      });
-      const { userData } = response.data;
-      this.setState({
-        isGravatarEnabled: userData.isGravatarEnabled,
-      });
-    }
-    catch (err) {
-      this.setState({ retrieveError: err });
-      logger.error(err);
-      throw new Error('Failed to update profile image');
-    }
-  }
-
-  /**
-   * Upload image
-   */
-  async uploadAttachment(file) {
-    try {
-      const formData = new FormData();
-      formData.append('file', file);
-      formData.append('_csrf', this.appContainer.csrfToken);
-      const response = await apiPost('/attachments.uploadProfileImage', formData);
-      this.setState({ isUploadedPicture: true, uploadedPictureSrc: response.attachment.filePathProxied });
-    }
-    catch (err) {
-      this.setState({ retrieveError: err });
-      logger.error(err);
-      throw new Error('Failed to upload profile image');
-    }
-  }
-
-  /**
-   * Delete image
-   */
-  async deleteProfileImage() {
-    try {
-      await apiPost('/attachments.removeProfileImage', { _csrf: this.appContainer.csrfToken });
-      this.setState({ isUploadedPicture: false, uploadedPictureSrc: DEFAULT_IMAGE });
-    }
-    catch (err) {
-      this.setState({ retrieveError: err });
-      logger.error(err);
-      throw new Error('Failed to delete profile image');
-    }
-  }
-
   /**
    * Associate LDAP account
    */

+ 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 - 1
packages/app/src/client/services/page-operation.ts

@@ -3,7 +3,8 @@ import urljoin from 'url-join';
 import { SubscriptionStatusType } from '~/interfaces/subscription';
 
 import { toastError } from '../util/apiNotification';
-import { apiv3Put } from '../util/apiv3-client';
+import { apiv3Post, apiv3Put } from '../util/apiv3-client';
+
 
 export const toggleSubscribe = async(pageId: string, currentStatus: SubscriptionStatusType | undefined): Promise<void> => {
   try {
@@ -60,3 +61,10 @@ export const exportAsMarkdown = (pageId: string, revisionId: string, format: str
   url.searchParams.append('revisionId', revisionId);
   window.location.href = url.href;
 };
+
+/**
+ * send request to fix broken paths caused by unexpected events such as server shutdown while renaming page paths
+ */
+export const resumeRenameOperation = async(pageId: string): Promise<void> => {
+  await apiv3Post('/pages/resume-rename', { pageId });
+};

+ 14 - 16
packages/app/src/client/util/GrowiRenderer.js

@@ -40,9 +40,9 @@ export default class GrowiRenderer {
     }
     else {
       this.preProcessors = [
-        new EasyGrid(appContainer),
-        new Linker(appContainer),
-        new CsvToTable(appContainer),
+        new EasyGrid(),
+        new Linker(),
+        new CsvToTable(),
         new XssFilter(appContainer),
       ];
       this.postProcessors = [
@@ -70,10 +70,10 @@ export default class GrowiRenderer {
     this.markdownItConfigurers = [
       new LinkerByRelativePathConfigurer(appContainer),
       new TaskListsConfigurer(appContainer),
-      new HeaderConfigurer(appContainer),
-      new EmojiConfigurer(appContainer),
+      new HeaderConfigurer(),
+      new EmojiConfigurer(),
       new MathJaxConfigurer(appContainer),
-      new DrawioViewerConfigurer(appContainer),
+      new DrawioViewerConfigurer(),
       new PlantUMLConfigurer(appContainer),
       new BlockdiagConfigurer(appContainer),
     ];
@@ -81,29 +81,27 @@ export default class GrowiRenderer {
     // add configurers according to mode
     switch (mode) {
       case 'page': {
-        const pageContainer = appContainer.getContainer('PageContainer');
-
         this.markdownItConfigurers = this.markdownItConfigurers.concat([
-          new FooternoteConfigurer(appContainer),
+          new FooternoteConfigurer(),
           new TocAndAnchorConfigurer(),
-          new HeaderLineNumberConfigurer(appContainer),
-          new HeaderWithEditLinkConfigurer(appContainer),
-          new TableWithHandsontableButtonConfigurer(appContainer),
+          new HeaderLineNumberConfigurer(),
+          new HeaderWithEditLinkConfigurer(),
+          new TableWithHandsontableButtonConfigurer(),
         ]);
         break;
       }
       case 'editor':
         this.markdownItConfigurers = this.markdownItConfigurers.concat([
-          new FooternoteConfigurer(appContainer),
-          new HeaderLineNumberConfigurer(appContainer),
-          new TableConfigurer(appContainer),
+          new FooternoteConfigurer(),
+          new HeaderLineNumberConfigurer(),
+          new TableConfigurer(),
         ]);
         break;
       // case 'comment':
       //   break;
       default:
         this.markdownItConfigurers = this.markdownItConfigurers.concat([
-          new TableConfigurer(appContainer),
+          new TableConfigurer(),
         ]);
         break;
     }

+ 14 - 7
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,22 +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<T>(path: string, formData: FormData): Promise<T> {
+  if (formData.get('_csrf') == null && csrfToken != null) {
+    formData.append('_csrf', csrfToken);
+  }
+  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 });
 }

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

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

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

+ 3 - 2
packages/app/src/client/util/i18n.js

@@ -1,7 +1,8 @@
 import i18n from 'i18next';
 import LanguageDetector from 'i18next-browser-languagedetector';
 import { initReactI18next } from 'react-i18next';
-import locales from '^/resource/locales';
+
+import locales from '^/public/static/locales';
 
 const aliasesMapping = {};
 Object.values(locales).forEach((locale) => {
@@ -13,7 +14,7 @@ Object.values(locales).forEach((locale) => {
   });
 });
 
-// extract metadata list from 'resource/locales/${locale}/meta.json'
+// extract metadata list from 'public/static/locales/${locale}/meta.json'
 export const localeMetadatas = Object.values(locales).map(locale => locale.meta);
 
 export const i18nFactory = (userLocaleId) => {

+ 0 - 4
packages/app/src/client/util/markdown-it/footernote.js

@@ -1,9 +1,5 @@
 export default class FooternoteConfigurer {
 
-  constructor(crowi) {
-    this.crowi = crowi;
-  }
-
   configure(md) {
     md.use(require('markdown-it-footnote'));
   }

+ 1 - 2
packages/app/src/client/util/markdown-it/header-line-number.js

@@ -1,7 +1,6 @@
 export default class HeaderLineNumberConfigurer {
 
-  constructor(crowi) {
-    this.crowi = crowi;
+  constructor() {
     this.firstLine = 0;
   }
 

+ 0 - 4
packages/app/src/client/util/markdown-it/header-with-edit-link.js

@@ -1,9 +1,5 @@
 export default class HeaderWithEditLinkConfigurer {
 
-  constructor(crowi) {
-    this.crowi = crowi;
-  }
-
   configure(md) {
     md.renderer.rules.heading_close = (tokens, idx) => {
       return `<span class="revision-head-edit-button">

+ 1 - 3
packages/app/src/client/util/markdown-it/header.js

@@ -1,8 +1,6 @@
 export default class HeaderConfigurer {
 
-  constructor(crowi) {
-    this.crowi = crowi;
-
+  constructor() {
     this.injectRevisionHeadClass = this.injectRevisionHeadClass.bind(this);
   }
 

+ 0 - 4
packages/app/src/client/util/markdown-it/table-with-handsontable-button.js

@@ -1,9 +1,5 @@
 export default class TableWithHandsontableButtonConfigurer {
 
-  constructor(crowi) {
-    this.crowi = crowi;
-  }
-
   configure(md) {
     md.renderer.rules.table_open = (tokens, idx) => {
       const beginLine = tokens[idx].map[0] + 1;

+ 0 - 4
packages/app/src/client/util/markdown-it/table.js

@@ -1,9 +1,5 @@
 export default class TableConfigurer {
 
-  constructor(crowi) {
-    this.crowi = crowi;
-  }
-
   configure(md) {
     md.renderer.rules.table_open = (tokens, idx) => {
       return '<table class="table table-bordered">';

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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