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

Merge branch 'fix/show-pagename-on-toaster' of https://github.com/weseek/growi into fix/show-pagename-on-toaster

keigo-h 3 лет назад
Родитель
Сommit
bb8fcad92a
100 измененных файлов с 1417 добавлено и 1089 удалено
  1. 0 5
      .github/workflows/release-rc.yml
  2. 0 5
      .github/workflows/release.yml
  3. 25 2
      CHANGELOG.md
  4. 1 1
      lerna.json
  5. 1 1
      package.json
  6. 0 34
      packages/app/bin/shrink-emojione-strategy.js
  7. 1 1
      packages/app/config/logger/config.dev.js
  8. 1 0
      packages/app/docker/Dockerfile
  9. 2 2
      packages/app/docker/README.md
  10. 9 7
      packages/app/package.json
  11. 15 0
      packages/app/resource/Contributor.js
  12. 2 9
      packages/app/resource/cdn-manifests.js
  13. 11 13
      packages/app/resource/locales/en_US/sandbox.md
  14. 35 1
      packages/app/resource/locales/en_US/translation.json
  15. 11 13
      packages/app/resource/locales/ja_JP/sandbox.md
  16. 35 1
      packages/app/resource/locales/ja_JP/translation.json
  17. 11 13
      packages/app/resource/locales/zh_CN/sandbox.md
  18. 36 2
      packages/app/resource/locales/zh_CN/translation.json
  19. 1 2
      packages/app/src/client/app.jsx
  20. 9 8
      packages/app/src/client/base.jsx
  21. 14 13
      packages/app/src/client/services/AdminAppContainer.js
  22. 6 5
      packages/app/src/client/services/AdminBasicSecurityContainer.js
  23. 11 11
      packages/app/src/client/services/AdminCustomizeContainer.js
  24. 5 5
      packages/app/src/client/services/AdminExternalAccountsContainer.js
  25. 9 8
      packages/app/src/client/services/AdminGeneralSecurityContainer.js
  26. 6 5
      packages/app/src/client/services/AdminGitHubSecurityContainer.js
  27. 16 13
      packages/app/src/client/services/AdminGoogleSecurityContainer.js
  28. 3 4
      packages/app/src/client/services/AdminHomeContainer.js
  29. 9 7
      packages/app/src/client/services/AdminImportContainer.js
  30. 5 3
      packages/app/src/client/services/AdminLdapSecurityContainer.js
  31. 5 2
      packages/app/src/client/services/AdminLocalSecurityContainer.js
  32. 7 5
      packages/app/src/client/services/AdminMarkDownContainer.js
  33. 10 6
      packages/app/src/client/services/AdminNotificationContainer.js
  34. 6 4
      packages/app/src/client/services/AdminOidcSecurityContainer.js
  35. 6 5
      packages/app/src/client/services/AdminSamlSecurityContainer.js
  36. 4 2
      packages/app/src/client/services/AdminSlackIntegrationLegacyContainer.js
  37. 6 4
      packages/app/src/client/services/AdminTwitterSecurityContainer.js
  38. 13 8
      packages/app/src/client/services/AdminUsersContainer.js
  39. 0 95
      packages/app/src/client/services/AppContainer.js
  40. 9 6
      packages/app/src/client/services/CommentContainer.js
  41. 3 0
      packages/app/src/client/services/ContextExtractor.tsx
  42. 2 77
      packages/app/src/client/services/EditorContainer.js
  43. 32 11
      packages/app/src/client/services/PageContainer.js
  44. 3 2
      packages/app/src/client/services/PageHistoryContainer.js
  45. 11 8
      packages/app/src/client/services/PersonalContainer.js
  46. 3 2
      packages/app/src/client/services/RevisionComparerContainer.js
  47. 3 1
      packages/app/src/client/services/TagContainer.js
  48. 1 1
      packages/app/src/client/util/editor.ts
  49. 0 0
      packages/app/src/client/util/emojione/emoji_strategy_shrinked.json
  50. 3 3
      packages/app/src/client/util/interceptor/drawio-interceptor.js
  51. 66 0
      packages/app/src/client/util/markdown-it/emoji-mart-data.ts
  52. 5 19
      packages/app/src/client/util/markdown-it/emoji.js
  53. 5 4
      packages/app/src/components/Admin/Customize/CustomizeLayoutSetting.jsx
  54. 11 8
      packages/app/src/components/Admin/ElasticsearchManagement/ElasticsearchManagement.jsx
  55. 6 3
      packages/app/src/components/Admin/ExportArchiveData/SelectCollectionsModal.jsx
  56. 10 7
      packages/app/src/components/Admin/ExportArchiveDataPage.jsx
  57. 9 7
      packages/app/src/components/Admin/ImportData/GrowiArchive/ImportForm.jsx
  58. 5 2
      packages/app/src/components/Admin/ImportData/GrowiArchive/UploadForm.jsx
  59. 7 4
      packages/app/src/components/Admin/ImportData/GrowiArchiveSection.jsx
  60. 8 5
      packages/app/src/components/Admin/Notification/GlobalNotificationList.jsx
  61. 10 6
      packages/app/src/components/Admin/Notification/ManageGlobalNotification.jsx
  62. 7 5
      packages/app/src/components/Admin/Security/GoogleSecuritySettingContents.jsx
  63. 7 5
      packages/app/src/components/Admin/Security/LdapAuthTest.jsx
  64. 8 6
      packages/app/src/components/Admin/Security/ShareLinkSetting.jsx
  65. 12 8
      packages/app/src/components/Admin/SlackIntegration/CustomBotWithProxySettings.jsx
  66. 8 4
      packages/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySecretTokenSection.jsx
  67. 9 5
      packages/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx
  68. 5 3
      packages/app/src/components/Admin/SlackIntegration/ManageCommandsProcess.jsx
  69. 5 3
      packages/app/src/components/Admin/SlackIntegration/ManageCommandsProcessWithoutProxy.jsx
  70. 12 7
      packages/app/src/components/Admin/SlackIntegration/OfficialBotSettings.jsx
  71. 17 11
      packages/app/src/components/Admin/SlackIntegration/SlackIntegration.jsx
  72. 13 13
      packages/app/src/components/Admin/SlackIntegration/WithProxyAccordions.jsx
  73. 7 4
      packages/app/src/components/Admin/UserGroupDetail/UserGroupPageList.jsx
  74. 5 2
      packages/app/src/components/Admin/Users/PasswordResetModal.jsx
  75. 8 4
      packages/app/src/components/Admin/Users/SendInvitationEmailButton.jsx
  76. 6 2
      packages/app/src/components/ArchiveCreateModal.jsx
  77. 5 2
      packages/app/src/components/CustomNavigation/CustomNav.jsx
  78. 4 1
      packages/app/src/components/CustomNavigation/CustomNavAndContents.jsx
  79. 61 0
      packages/app/src/components/EmptyTrashButton.tsx
  80. 0 71
      packages/app/src/components/EmptyTrashModal.jsx
  81. 92 0
      packages/app/src/components/EmptyTrashModal.tsx
  82. 7 4
      packages/app/src/components/MaintenanceModeContent.tsx
  83. 2 1
      packages/app/src/components/Me/ApiSettings.jsx
  84. 6 25
      packages/app/src/components/Me/EditorSettings.tsx
  85. 6 11
      packages/app/src/components/Me/PasswordSettings.jsx
  86. 6 7
      packages/app/src/components/MyDraftList/MyDraftList.jsx
  87. 174 0
      packages/app/src/components/Navbar/AppearanceModeDropdown.tsx
  88. 46 25
      packages/app/src/components/Navbar/GrowiNavbar.tsx
  89. 3 1
      packages/app/src/components/Navbar/PageEditorModeManager.jsx
  90. 1 164
      packages/app/src/components/Navbar/PersonalDropdown.jsx
  91. 28 22
      packages/app/src/components/Page.jsx
  92. 7 4
      packages/app/src/components/Page/RevisionLoader.jsx
  93. 15 3
      packages/app/src/components/Page/RevisionRenderer.jsx
  94. 2 2
      packages/app/src/components/Page/TagsInput.tsx
  95. 5 2
      packages/app/src/components/PageAttachment.jsx
  96. 9 7
      packages/app/src/components/PageComment.tsx
  97. 32 17
      packages/app/src/components/PageComment/CommentEditor.jsx
  98. 1 3
      packages/app/src/components/PageDeleteModal.tsx
  99. 79 68
      packages/app/src/components/PageEditor.jsx
  100. 138 66
      packages/app/src/components/PageEditor/CodeMirrorEditor.jsx

+ 0 - 5
.github/workflows/release-rc.yml

@@ -54,8 +54,3 @@ jobs:
         cache-from: type=gha
         cache-from: type=gha
         cache-to: type=gha,mode=max
         cache-to: type=gha,mode=max
         tags: ${{ steps.meta.outputs.tags }}
         tags: ${{ steps.meta.outputs.tags }}
-
-    - name: Move cache
-      run: |
-        rm -rf /tmp/.buildx-cache
-        mv /tmp/.buildx-cache-new /tmp/.buildx-cache

+ 0 - 5
.github/workflows/release.yml

@@ -183,11 +183,6 @@ jobs:
         cache-to: type=gha,mode=max
         cache-to: type=gha,mode=max
         tags: ${{ steps.meta.outputs.tags }}
         tags: ${{ steps.meta.outputs.tags }}
 
 
-    - name: Move cache
-      run: |
-        rm -rf /tmp/.buildx-cache
-        mv /tmp/.buildx-cache-new /tmp/.buildx-cache
-
     - name: Update Docker Hub Description
     - name: Update Docker Hub Description
       uses: peter-evans/dockerhub-description@v3
       uses: peter-evans/dockerhub-description@v3
       with:
       with:

+ 25 - 2
CHANGELOG.md

@@ -1,9 +1,33 @@
 # Changelog
 # Changelog
 
 
-## [Unreleased](https://github.com/weseek/growi/compare/v5.0.4...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v5.0.5...HEAD)
 
 
 *Please do not manually update this file. We've automated the process.*
 *Please do not manually update this file. We've automated the process.*
 
 
+## [v5.0.5](https://github.com/weseek/growi/compare/v5.0.4...v5.0.5) - 2022-05-16
+
+### 💎 Features
+
+- feat: Empty trash button in trash page (#5816) @yukendev
+
+### 🚀 Improvement
+
+- imprv: Count badge colors (#5835) @shukmos
+- imprv: List group background colors on PageTree (#5812) @shukmos
+- imprv: Page path auto complete function for page rename modal (#5805) @kaoritokashiki
+- imprv: Show toastr when converting is completed on Private Legacy Page (#5810) @yukendev
+- imprv: Create parent pages as needed by path that includes slash (#5809) @kaoritokashiki
+
+### 🐛 Bug Fixes
+
+- fix: Change the execution user of the official docker image to root (#5846) @yuki-takei
+- fix: Display admin link only with logged in (#5799) @hirokei-camel
+- fix: Error when renaming (#5793) @miya
+
+### 🧰 Maintenance
+
+- support: Typescriptize tag model (#5778) @kaoritokashiki
+
 ## [v5.0.4](https://github.com/weseek/growi/compare/v5.0.3...v5.0.4) - 2022-04-28
 ## [v5.0.4](https://github.com/weseek/growi/compare/v5.0.3...v5.0.4) - 2022-04-28
 
 
 ### 💎 Features
 ### 💎 Features
@@ -177,7 +201,6 @@
 - support: update nanoid yarn.lock v3.1.30 to v3.2.0 (#5216) @LuqmanHakim-Grune
 - support: update nanoid yarn.lock v3.1.30 to v3.2.0 (#5216) @LuqmanHakim-Grune
 - support: update validator version (#5562) @LuqmanHakim-Grune
 - support: update validator version (#5562) @LuqmanHakim-Grune
 
 
-
 ## [v4.5.15](https://github.com/weseek/growi/compare/v4.5.14...v4.5.15) - 2022-02-17
 ## [v4.5.15](https://github.com/weseek/growi/compare/v4.5.14...v4.5.15) - 2022-02-17
 
 
 ### 🚀 Improvement
 ### 🚀 Improvement

+ 1 - 1
lerna.json

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

+ 1 - 1
package.json

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

+ 0 - 34
packages/app/bin/shrink-emojione-strategy.js

@@ -1,34 +0,0 @@
-/**
- * the tool to shrink emojione/emoji_strategy.json and output
- *
- * @author Yuki Takei <yuki@weseek.co.jp>
- */
-/*
-require('module-alias/register');
-
-const fs = require('graceful-fs');
-
-const helpers = require('@commons/util/helpers');
-
-const emojiStrategy = require('emojione/emoji_strategy.json');
-const markdownItEmojiFull = require('markdown-it-emoji/lib/data/full.json');
-
-const OUT = helpers.root('tmp/emoji_strategy_shrinked.json');
-
-const shrinkedMap = {};
-Object.keys(emojiStrategy).forEach((unicode) => {
-  const data = emojiStrategy[unicode];
-  const shortname = data.shortname.replace(/:/g, '');
-
-  // ignore if it isn't included in markdownItEmojiFull
-  if (markdownItEmojiFull[shortname] == null) {
-    return;
-  }
-
-  // add
-  shrinkedMap[unicode] = data;
-});
-
-// write
-fs.writeFileSync(OUT, JSON.stringify(shrinkedMap));
-*/

+ 1 - 1
packages/app/config/logger/config.dev.js

@@ -12,7 +12,7 @@ module.exports = {
   // 'growi:crow:dev': 'debug',
   // 'growi:crow:dev': 'debug',
   'growi:crowi:express-init': 'debug',
   'growi:crowi:express-init': 'debug',
   'growi:models:external-account': 'debug',
   'growi:models:external-account': 'debug',
-  // 'growi:routes:login': 'debug',
+  'growi:routes:login': 'debug',
   'growi:routes:login-passport': 'debug',
   'growi:routes:login-passport': 'debug',
   'growi:middleware:safe-redirect': 'debug',
   'growi:middleware:safe-redirect': 'debug',
   'growi:service:PassportService': 'debug',
   'growi:service:PassportService': 'debug',

+ 1 - 0
packages/app/docker/Dockerfile

@@ -159,6 +159,7 @@ RUN rm node_modules.tar packages.tar
 
 
 COPY --chown=node:node --chmod=700 packages/app/docker/docker-entrypoint.sh /
 COPY --chown=node:node --chmod=700 packages/app/docker/docker-entrypoint.sh /
 
 
+USER root
 WORKDIR ${appDir}/packages/app
 WORKDIR ${appDir}/packages/app
 
 
 VOLUME /data
 VOLUME /data

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

@@ -10,8 +10,8 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 ------------------------------------------------
 
 
-* [`5.0.4`, `5.0`, `5`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.4/docker/Dockerfile)
-* [`5.0.4-nocdn`, `5.0-nocdn`, `5-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.4/docker/Dockerfile)
+* [`5.0.5`, `5.0`, `5`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.5/docker/Dockerfile)
+* [`5.0.5-nocdn`, `5.0-nocdn`, `5-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.5/docker/Dockerfile)
 * [`4.5.15`, `4.5`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.15/docker/Dockerfile)
 * [`4.5.15`, `4.5`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.15/docker/Dockerfile)
 * [`4.5.15-nocdn`, `4.5-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.15/docker/Dockerfile)
 * [`4.5.15-nocdn`, `4.5-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.15/docker/Dockerfile)
 * [`4.4.13`, `4.4` (Dockerfile)](https://github.com/weseek/growi/blob/v4.4.13/docker/Dockerfile)
 * [`4.4.13`, `4.4` (Dockerfile)](https://github.com/weseek/growi/blob/v4.4.13/docker/Dockerfile)

+ 9 - 7
packages/app/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/app",
   "name": "@growi/app",
-  "version": "5.0.5-RC.0",
+  "version": "5.0.6-RC.0",
   "license": "MIT",
   "license": "MIT",
   "scripts": {
   "scripts": {
     "//// for production": "",
     "//// for production": "",
@@ -62,11 +62,11 @@
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@godaddy/terminus": "^4.9.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^5.0.5-RC.0",
-    "@growi/plugin-attachment-refs": "^5.0.5-RC.0",
-    "@growi/plugin-lsx": "^5.0.5-RC.0",
-    "@growi/plugin-pukiwiki-like-linker": "^5.0.5-RC.0",
-    "@growi/slack": "^5.0.5-RC.0",
+    "@growi/codemirror-textlint": "^5.0.6-RC.0",
+    "@growi/plugin-attachment-refs": "^5.0.6-RC.0",
+    "@growi/plugin-lsx": "^5.0.6-RC.0",
+    "@growi/plugin-pukiwiki-like-linker": "^5.0.6-RC.0",
+    "@growi/slack": "^5.0.6-RC.0",
     "@promster/express": "^7.0.2",
     "@promster/express": "^7.0.2",
     "@promster/server": "^7.0.4",
     "@promster/server": "^7.0.4",
     "@slack/events-api": "^3.0.0",
     "@slack/events-api": "^3.0.0",
@@ -167,7 +167,7 @@
   },
   },
   "devDependencies": {
   "devDependencies": {
     "@alienfast/i18next-loader": "^1.1.4",
     "@alienfast/i18next-loader": "^1.1.4",
-    "@growi/ui": "^5.0.5-RC.0",
+    "@growi/ui": "^5.0.6-RC.0",
     "@handsontable/react": "=2.1.0",
     "@handsontable/react": "=2.1.0",
     "@types/compression": "^1.7.0",
     "@types/compression": "^1.7.0",
     "@types/express": "^4.17.11",
     "@types/express": "^4.17.11",
@@ -187,6 +187,8 @@
     "csv-to-markdown-table": "^1.0.1",
     "csv-to-markdown-table": "^1.0.1",
     "diff2html": "^3.1.2",
     "diff2html": "^3.1.2",
     "eazy-logger": "^3.1.0",
     "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-cypress": "^2.12.1",
     "eslint-plugin-regex": "^1.8.0",
     "eslint-plugin-regex": "^1.8.0",
     "file-loader": "^5.0.2",
     "file-loader": "^5.0.2",

+ 15 - 0
packages/app/resource/Contributor.js

@@ -12,6 +12,11 @@ const contributors = [
           { position: 'Soncho 2nd', name: 'yusuketk' },
           { position: 'Soncho 2nd', name: 'yusuketk' },
           { position: 'Paladin', name: 'itizawa' },
           { position: 'Paladin', name: 'itizawa' },
           { position: 'Valkyrie', name: 'kaoritokashiki' },
           { position: 'Valkyrie', name: 'kaoritokashiki' },
+          { position: 'Slime', name: 'TatsuyaIse' },
+          { position: 'Knight', name: 'Yohei-Shiina' },
+          { position: 'Titan', name: 'ryoh15' },
+          { position: 'Haberion', name: 'hakumizuki' },
+          { position: 'Undefined', name: 'miya' },
         ],
         ],
       },
       },
       {
       {
@@ -37,6 +42,7 @@ const contributors = [
           { name: 'ryuichi-e' },
           { name: 'ryuichi-e' },
           { name: 'N1koge' },
           { name: 'N1koge' },
           { name: 'Ertai87' },
           { name: 'Ertai87' },
+          { name: 'takayuki-t' },
           { name: 'zahmis' },
           { name: 'zahmis' },
           { name: 'takeru0001' },
           { name: 'takeru0001' },
           { name: 'Shu Katabe' },
           { name: 'Shu Katabe' },
@@ -46,6 +52,15 @@ const contributors = [
           { name: 'stevenfukase' },
           { name: 'stevenfukase' },
           { name: 'miya' },
           { name: 'miya' },
           { name: 'kaho819' },
           { name: 'kaho819' },
+          { name: 'yuto-oweseek' },
+          { name: 'maow89126' },
+          { name: 'kntowd' },
+          { name: 'yukendev' },
+          { name: 'asami-n' },
+          { name: 'ryohi15' },
+          { name: 'yoshiro-s' },
+          { name: 'kuimac' },
+          { name: 'akira-sugiyama' },
         ],
         ],
       },
       },
     ],
     ],

+ 2 - 9
packages/app/resource/cdn-manifests.js

@@ -3,7 +3,7 @@ module.exports = {
     {
     {
       name: 'basis',
       name: 'basis',
       // eslint-disable-next-line max-len
       // eslint-disable-next-line max-len
-      url: 'https://cdn.jsdelivr.net/combine/npm/emojione@3.1.2,npm/jquery@3.4.0,npm/popper.js@1.15.0,npm/bootstrap@4.5.0/dist/js/bootstrap.min.js,npm/scrollpos-styler@0.7.1,npm/jquery-slimscroll@1.3.8/jquery.slimscroll.min.js',
+      url: 'https://cdn.jsdelivr.net/combine/npm/jquery@3.4.0,npm/popper.js@1.15.0,npm/bootstrap@4.5.0/dist/js/bootstrap.min.js,npm/scrollpos-styler@0.7.1,npm/jquery-slimscroll@1.3.8/jquery.slimscroll.min.js',
       groups: ['basis'],
       groups: ['basis'],
       args: {
       args: {
         integrity: '',
         integrity: '',
@@ -138,14 +138,7 @@ module.exports = {
         integrity: '',
         integrity: '',
       },
       },
     },
     },
-    {
-      name: 'emojione',
-      url: 'https://cdn.jsdelivr.net/npm/emojione@3.1.2/extras/css/emojione.min.css',
-      groups: ['basis'],
-      args: {
-        integrity: '',
-      },
-    },
+
     {
     {
       name: 'animate.css',
       name: 'animate.css',
       url: 'https://cdn.jsdelivr.net/npm/animate.css@3.7.2/animate.min.css',
       url: 'https://cdn.jsdelivr.net/npm/animate.css@3.7.2/animate.min.css',

+ 11 - 13
packages/app/resource/locales/en_US/sandbox.md

@@ -12,7 +12,7 @@
   </div>
   </div>
 </div>
 </div>
 
 
-# :pencil: Block Elements
+# :memo: Block Elements
 
 
 ## Headers
 ## Headers
 
 
@@ -160,7 +160,7 @@ ___
 
 
 
 
 
 
-# :pencil: Typography
+# :memo: Typography
 
 
 ## Strong Text
 ## Strong Text
 
 
@@ -200,7 +200,7 @@ This is ___Italic & Bold___.
 This is ***Italic & Bold***.
 This is ***Italic & Bold***.
 This is ___Italic & Bold___.
 This is ___Italic & Bold___.
 
 
-# :pencil: Images
+# :memo: Images
 
 
 You can insert `<img>` tag using `![description](URL)`.
 You can insert `<img>` tag using `![description](URL)`.
 
 
@@ -221,7 +221,7 @@ The size of the image can be set by using an HTML image tag
 <img src="https://octodex.github.com/images/dojocat.jpg" width="200px">
 <img src="https://octodex.github.com/images/dojocat.jpg" width="200px">
 
 
 
 
-# :pencil: Link
+# :memo: Link
 
 
 ## Markdown standard
 ## Markdown standard
 
 
@@ -259,7 +259,7 @@ Example of Bootstrap4 is [[here>./Bootstrap4]]
 [[./Bootstrap4]]  
 [[./Bootstrap4]]  
 Example of Bootstrap4 is[[here>./Bootstrap4]]
 Example of Bootstrap4 is[[here>./Bootstrap4]]
 
 
-# :pencil: Lists
+# :memo: Lists
 
 
 ## Ul Bulleted list
 ## Ul Bulleted list
 
 
@@ -319,7 +319,7 @@ The numbers don’t have to be in numerical order, but the list should start wit
 - [x] Task2
 - [x] Task2
 
 
 
 
-# :pencil: Table
+# :memo: Table
 
 
 ## Markdown Standard
 ## Markdown Standard
 
 
@@ -415,7 +415,7 @@ Content Cell,Content Cell
 :::
 :::
 
 
 
 
-# :pencil: Footnote
+# :memo: Footnote
 
 
 You can write a reference [^1] to a footnote. You can also add an inline footnote^[Inline_footnote].
 You can write a reference [^1] to a footnote. You can also add an inline footnote^[Inline_footnote].
 
 
@@ -428,15 +428,13 @@ Long footnotes can be written as [^longnote].
     Subsequent paragraphs are indented and belong to the previous footnote.
     Subsequent paragraphs are indented and belong to the previous footnote.
 
 
 
 
-# :pencil: Emoji
-
-See [emojione](https://www.emojione.com/)
+# :memo: Emoji
 
 
 :smiley: :smile: :laughing: :innocent: :drooling_face:
 :smiley: :smile: :laughing: :innocent: :drooling_face:
 
 
-:family: :family_man_boy: :family_man_girl: :family_man_girl_girl: :family_woman_girl_girl:
+:family: :man-boy: :man-girl: :man-girl-girl: :woman-girl-girl:
 
 
-:thumbsup: :thumbsdown: :open_hands: :raised_hands: :point_right:
+:+1: :-1: :open_hands: :raised_hands: :point_right:
 
 
 :apple: :green_apple: :strawberry: :cake: :hamburger:
 :apple: :green_apple: :strawberry: :cake: :hamburger:
 
 
@@ -444,7 +442,7 @@ See [emojione](https://www.emojione.com/)
 
 
 :hearts: :broken_heart: :heartbeat: :heartpulse: :heart_decoration:
 :hearts: :broken_heart: :heartbeat: :heartpulse: :heart_decoration:
 
 
-:watch: :gear: :gem: :wrench: :envelope:
+:watch: :gear: :gem: :wrench: :email:
 
 
 
 
 # :heavy_plus_sign: More..
 # :heavy_plus_sign: More..

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

@@ -390,7 +390,8 @@
     }
     }
   },
   },
   "page_comment": {
   "page_comment": {
-    "display_the_page_when_posting_this_comment": "Display the page when posting this comment"
+    "display_the_page_when_posting_this_comment": "Display the page when posting this comment",
+    "no_user_found": "No user found"
   },
   },
   "page_api_error": {
   "page_api_error": {
     "notfound_or_forbidden": "Original page is not found or forbidden.",
     "notfound_or_forbidden": "Original page is not found or forbidden.",
@@ -442,8 +443,11 @@
   "deleted_pages": "{{path}} has been deleted",
   "deleted_pages": "{{path}} has been deleted",
   "deleted_pages_completely": "{{path}} has been deleted completely",
   "deleted_pages_completely": "{{path}} has been deleted completely",
   "renamed_pages": "{{path}} has been renamed",
   "renamed_pages": "{{path}} has been renamed",
+  "empty_trash": "The trash has been emptied",
   "modal_empty":{
   "modal_empty":{
     "empty_the_trash": "Empty The Trash",
     "empty_the_trash": "Empty The Trash",
+    "empty_the_trash_button": "Empty The Trash",
+    "not_deletable_notice": "Some pages cannot be removed due to lack of permission.",
     "notice": "The pages deleted completely are unrecoverable."
     "notice": "The pages deleted completely are unrecoverable."
   },
   },
   "modal_duplicate": {
   "modal_duplicate": {
@@ -1020,6 +1024,36 @@
     "incorrect_token_or_expired_url": "The token is incorrect or the URL has expired. Please resend a password reset request via the link below.",
     "incorrect_token_or_expired_url": "The token is incorrect or the URL has expired. Please resend a password reset request via the link below.",
     "password_and_confirm_password_does_not_match": "Password and confirm password does not match"
     "password_and_confirm_password_does_not_match": "Password and confirm password does not match"
   },
   },
+  "emoji" :{
+    "title": "Pick an Emoji",
+    "search": "Search",
+    "clear": "Clear",
+    "notfound": "No Emoji Found",
+    "skintext": "Choose your default skin tone",
+    "categories": {
+      "search": "Search Results",
+      "recent": "Frequently Used",
+      "smileys": "Smileys & Emotion",
+      "people": "People & Body",
+      "nature": "Animals & Nature",
+      "foods": "Food & Drink",
+      "activity": "Activity",
+      "places": "Travel & Places",
+      "objects": "Objects",
+      "symbols": "Symbols",
+      "flags": "Flags",
+      "custom": "Custom"
+    },
+    "categorieslabel": "Emoji categories",
+    "skintones": {
+      "1": "Default Skin Tone",
+      "2": "Light Skin Tone",
+      "3": "Medium-Light Skin Tone",
+      "4": "Medium Skin Tone",
+      "5": "Medium-Dark Skin Tone",
+      "6": "Dark Skin Tone"
+    }
+  },
   "maintenance_mode":{
   "maintenance_mode":{
     "maintenance_mode": "Maintenance Mode",
     "maintenance_mode": "Maintenance Mode",
     "growi_is_under_maintenance": "GROWI is under maintenance. Please wait until it ends.",
     "growi_is_under_maintenance": "GROWI is under maintenance. Please wait until it ends.",

+ 11 - 13
packages/app/resource/locales/ja_JP/sandbox.md

@@ -12,7 +12,7 @@
   </div>
   </div>
 </div>
 </div>
 
 
-# :pencil: Block Elements
+# :memo: Block Elements
 
 
 ## Headers 見出し
 ## Headers 見出し
 
 
@@ -159,7 +159,7 @@ ___
 
 
 
 
 
 
-# :pencil: Typography
+# :memo: Typography
 
 
 ## 強調
 ## 強調
 
 
@@ -199,7 +199,7 @@ ___
 これは ***イタリック&ボールド*** です
 これは ***イタリック&ボールド*** です
 これは ___イタリック&ボールド___ です
 これは ___イタリック&ボールド___ です
 
 
-# :pencil: Images
+# :memo: Images
 
 
 `![Alt文字列](URL)` で`<img>`タグを挿入できます。
 `![Alt文字列](URL)` で`<img>`タグを挿入できます。
 
 
@@ -220,7 +220,7 @@ ___
 <img src="https://octodex.github.com/images/dojocat.jpg" width="200px">
 <img src="https://octodex.github.com/images/dojocat.jpg" width="200px">
 
 
 
 
-# :pencil: Link
+# :memo: Link
 
 
 ## Markdown 標準
 ## Markdown 標準
 
 
@@ -258,7 +258,7 @@ Bootstrap4のExampleは[[こちら>./Bootstrap4]]
 [[./Bootstrap4]]  
 [[./Bootstrap4]]  
 Bootstrap4のExampleは[[こちら>./Bootstrap4]]
 Bootstrap4のExampleは[[こちら>./Bootstrap4]]
 
 
-# :pencil: Lists
+# :memo: Lists
 
 
 ## Ul 箇条書きリスト
 ## Ul 箇条書きリスト
 
 
@@ -318,7 +318,7 @@ Bootstrap4のExampleは[[こちら>./Bootstrap4]]
 - [x] タスク2
 - [x] タスク2
 
 
 
 
-# :pencil: Table
+# :memo: Table
 
 
 ## Markdown 標準
 ## Markdown 標準
 
 
@@ -414,7 +414,7 @@ Content Cell,Content Cell
 :::
 :::
 
 
 
 
-# :pencil: Footnote
+# :memo: Footnote
 
 
 脚注への参照[^1]を書くことができます。また、インラインの脚注^[インラインで記述できる脚注です]を入れる事も出来ます。
 脚注への参照[^1]を書くことができます。また、インラインの脚注^[インラインで記述できる脚注です]を入れる事も出来ます。
 
 
@@ -427,15 +427,13 @@ Content Cell,Content Cell
     後続の段落はインデントされて、前の脚注に属します。
     後続の段落はインデントされて、前の脚注に属します。
 
 
 
 
-# :pencil: Emoji
-
-See [emojione](https://www.emojione.com/)
+# :memo: Emoji
 
 
 :smiley: :smile: :laughing: :innocent: :drooling_face:
 :smiley: :smile: :laughing: :innocent: :drooling_face:
 
 
-:family: :family_man_boy: :family_man_girl: :family_man_girl_girl: :family_woman_girl_girl:
+:family: :man-boy: :man-girl: :man-girl-girl: :woman-girl-girl:
 
 
-:thumbsup: :thumbsdown: :open_hands: :raised_hands: :point_right:
+:+1: :-1: :open_hands: :raised_hands: :point_right:
 
 
 :apple: :green_apple: :strawberry: :cake: :hamburger:
 :apple: :green_apple: :strawberry: :cake: :hamburger:
 
 
@@ -443,7 +441,7 @@ See [emojione](https://www.emojione.com/)
 
 
 :hearts: :broken_heart: :heartbeat: :heartpulse: :heart_decoration:
 :hearts: :broken_heart: :heartbeat: :heartpulse: :heart_decoration:
 
 
-:watch: :gear: :gem: :wrench: :envelope:
+:watch: :gear: :gem: :wrench: :email:
 
 
 
 
 
 

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

@@ -390,7 +390,8 @@
     }
     }
   },
   },
   "page_comment": {
   "page_comment": {
-    "display_the_page_when_posting_this_comment": "投稿時のページを表示する"
+    "display_the_page_when_posting_this_comment": "投稿時のページを表示する",
+    "no_user_found": "ユーザー名が見つかりません"
   },
   },
   "page_api_error": {
   "page_api_error": {
     "notfound_or_forbidden": "元のページが見つからないか、アクセス権がありません。",
     "notfound_or_forbidden": "元のページが見つからないか、アクセス権がありません。",
@@ -442,8 +443,11 @@
   "deleted_pages": "{{path}} をゴミ箱に入れました",
   "deleted_pages": "{{path}} をゴミ箱に入れました",
   "deleted_pages_completely": "{{path}} を完全に削除しました",
   "deleted_pages_completely": "{{path}} を完全に削除しました",
   "renamed_pages": "{{path}} を移動/名前変更しました",
   "renamed_pages": "{{path}} を移動/名前変更しました",
+  "empty_trash": "ゴミ箱を空にしました",
   "modal_empty":{
   "modal_empty":{
     "empty_the_trash": "ゴミ箱を空にする",
     "empty_the_trash": "ゴミ箱を空にする",
+    "empty_the_trash_button": "空にする",
+    "not_deletable_notice": "権限がないため、いくつかのページは削除できません",
     "notice": "完全削除したページは元に戻すことができません"
     "notice": "完全削除したページは元に戻すことができません"
   },
   },
   "modal_duplicate": {
   "modal_duplicate": {
@@ -1013,6 +1017,36 @@
     "incorrect_token_or_expired_url":"トークンが正しくないか、URLの有効期限が切れています。 以下のリンクからパスワードリセットリクエストを再送信してください。",
     "incorrect_token_or_expired_url":"トークンが正しくないか、URLの有効期限が切れています。 以下のリンクからパスワードリセットリクエストを再送信してください。",
     "password_and_confirm_password_does_not_match": "パスワードと確認パスワードが一致しません"
     "password_and_confirm_password_does_not_match": "パスワードと確認パスワードが一致しません"
   },
   },
+  "emoji" :{
+    "title": "絵文字を選択",
+    "search": "探す",
+    "clear": "リセット",
+    "notfound": "絵文字が見つかりません",
+    "skintext": "デフォルトの肌の色を選択",
+    "categories": {
+      "search": "検索結果",
+      "recent": "最新履歴",
+      "smileys": "スマイリーと感情",
+      "people": "人と体",
+      "nature": "動物と自然",
+      "foods": "食べ物や飲み物",
+      "activity": "アクティビティ",
+      "places": "旅行と場所",
+      "objects": "オブジェクト",
+      "symbols": "シンボル",
+      "flags": "国旗",
+      "custom": "カスタマイズ"
+    },
+    "categorieslabel": "絵文字カテゴリ",
+    "skintones": {
+      "1": "デフォルトの肌の色",
+      "2": "明るい肌のトーン",
+      "3": "ミディアム-明るい肌のトーン",
+      "4": "ミディアムスキントーン",
+      "5": "ミディアムダークスキントーン",
+      "6": "肌の色が濃い"
+    }
+  },
   "maintenance_mode":{
   "maintenance_mode":{
     "maintenance_mode": "メンテナンスモード",
     "maintenance_mode": "メンテナンスモード",
     "growi_is_under_maintenance": "GROWI はメンテナンス中です。終了するまでお待ちください",
     "growi_is_under_maintenance": "GROWI はメンテナンス中です。終了するまでお待ちください",

+ 11 - 13
packages/app/resource/locales/zh_CN/sandbox.md

@@ -12,7 +12,7 @@
   </div>
   </div>
 </div>
 </div>
 
 
-# :pencil: Block Elements
+# :memo: Block Elements
 
 
 ## Headers
 ## Headers
 
 
@@ -160,7 +160,7 @@ ___
 
 
 
 
 
 
-# :pencil: Typography
+# :memo: Typography
 
 
 ## Strong Text
 ## Strong Text
 
 
@@ -200,7 +200,7 @@ This is ___Italic & Bold___.
 This is ***Italic & Bold***.
 This is ***Italic & Bold***.
 This is ___Italic & Bold___.
 This is ___Italic & Bold___.
 
 
-# :pencil: Images
+# :memo: Images
 
 
 You can insert `<img>` tag using `![description](URL)`.
 You can insert `<img>` tag using `![description](URL)`.
 
 
@@ -221,7 +221,7 @@ The size of the image can be set by using an HTML image tag
 <img src="https://octodex.github.com/images/dojocat.jpg" width="200px">
 <img src="https://octodex.github.com/images/dojocat.jpg" width="200px">
 
 
 
 
-# :pencil: Link
+# :memo: Link
 
 
 ## Markdown standard
 ## Markdown standard
 
 
@@ -259,7 +259,7 @@ Example of Bootstrap4 is[[here>./Bootstrap4]]
 [[./Bootstrap4]]  
 [[./Bootstrap4]]  
 Example of Bootstrap4 is [[here>./Bootstrap4]]
 Example of Bootstrap4 is [[here>./Bootstrap4]]
 
 
-# :pencil: Lists
+# :memo: Lists
 
 
 ## Ul Bulleted list
 ## Ul Bulleted list
 
 
@@ -319,7 +319,7 @@ The numbers don’t have to be in numerical order, but the list should start wit
 - [x] Task2
 - [x] Task2
 
 
 
 
-# :pencil: Table
+# :memo: Table
 
 
 ## Markdown Standard
 ## Markdown Standard
 
 
@@ -415,7 +415,7 @@ Content Cell,Content Cell
 :::
 :::
 
 
 
 
-# :pencil: Footnote
+# :memo: Footnote
 
 
 You can write a reference [^1] to a footnote. You can also add an inline footnote^[Inline_footnote].
 You can write a reference [^1] to a footnote. You can also add an inline footnote^[Inline_footnote].
 
 
@@ -428,15 +428,13 @@ Long footnotes can be written as [^longnote].
     Subsequent paragraphs are indented and belong to the previous footnote.
     Subsequent paragraphs are indented and belong to the previous footnote.
 
 
 
 
-# :pencil: Emoji
-
-See [emojione](https://www.emojione.com/)
+# :memo: Emoji
 
 
 :smiley: :smile: :laughing: :innocent: :drooling_face:
 :smiley: :smile: :laughing: :innocent: :drooling_face:
 
 
-:family: :family_man_boy: :family_man_girl: :family_man_girl_girl: :family_woman_girl_girl:
+:family: :man-boy: :man-girl: :man-girl-girl: :woman-girl-girl:
 
 
-:thumbsup: :thumbsdown: :open_hands: :raised_hands: :point_right:
+:+1: :-1: :open_hands: :raised_hands: :point_right:
 
 
 :apple: :green_apple: :strawberry: :cake: :hamburger:
 :apple: :green_apple: :strawberry: :cake: :hamburger:
 
 
@@ -444,7 +442,7 @@ See [emojione](https://www.emojione.com/)
 
 
 :hearts: :broken_heart: :heartbeat: :heartpulse: :heart_decoration:
 :hearts: :broken_heart: :heartbeat: :heartpulse: :heart_decoration:
 
 
-:watch: :gear: :gem: :wrench: :envelope:
+:watch: :gear: :gem: :wrench: :email:
 
 
 
 
 # :heavy_plus_sign: More..
 # :heavy_plus_sign: More..

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

@@ -369,7 +369,8 @@
 		}
 		}
   },
   },
   "page_comment": {
   "page_comment": {
-    "display_the_page_when_posting_this_comment": "Display the page when posting this comment"
+    "display_the_page_when_posting_this_comment": "Display the page when posting this comment",
+    "no_user_found": "未找到用户名"
   },
   },
 	"page_api_error": {
 	"page_api_error": {
 		"notfound_or_forbidden": "未找到或禁止原始页。",
 		"notfound_or_forbidden": "未找到或禁止原始页。",
@@ -421,8 +422,11 @@
   "deleted_pages": "将 {{path}} 放入垃圾箱",
   "deleted_pages": "将 {{path}} 放入垃圾箱",
   "deleted_pages_completely": "{{path}} 已被完全删除",
   "deleted_pages_completely": "{{path}} 已被完全删除",
   "renamed_pages": "移动/重命名 {{path}}",
   "renamed_pages": "移动/重命名 {{path}}",
+  "empty_trash": "清空垃圾",
 	"modal_empty": {
 	"modal_empty": {
-		"empty_the_trash": "Empty The Trash",
+		"empty_the_trash": "清空垃圾",
+    "empty_the_trash_button": "清空垃圾",
+    "not_deletable_notice": "由于缺乏权限,一些页面不能被删除",
 		"notice": "完全删除的页面是不可恢复的。"
 		"notice": "完全删除的页面是不可恢复的。"
 	},
 	},
 	"modal_duplicate": {
 	"modal_duplicate": {
@@ -1023,6 +1027,36 @@
     "incorrect_token_or_expired_url":"令牌不正确或 URL 已过期。 请通过以下链接重新发送密码重置请求",
     "incorrect_token_or_expired_url":"令牌不正确或 URL 已过期。 请通过以下链接重新发送密码重置请求",
     "password_and_confirm_password_does_not_match": "密码和确认密码不匹配"
     "password_and_confirm_password_does_not_match": "密码和确认密码不匹配"
   },
   },
+  "emoji" :{
+    "title": "选择一个表情符号",
+    "search": "搜索",
+    "clear": "重置",
+    "notfound": "找不到表情符号",
+    "skintext": "选择您的默认肤色",
+    "categories": {
+      "search": "搜索结果",
+      "recent": "经常使用",
+      "smileys": "笑脸和情感",
+      "people": "人和身体",
+      "nature": "动物与自然",
+      "foods": "食物和饮料",
+      "activity": "活动",
+      "places": "旅行和地方",
+      "objects": "对象",
+      "symbols": "符号",
+      "flags": "旗帜",
+      "custom": "定制"
+    },
+    "categorieslabel": "表情符号类别",
+    "skintones": {
+      "1": "默认肤色",
+      "2": "浅肤色",
+      "3": "中浅肤色",
+      "4": "中等肤色",
+      "5": "中深肤色",
+      "6": "深色肤色"
+    }
+  },
   "maintenance_mode":{
   "maintenance_mode":{
     "maintenance_mode": "维护模式",
     "maintenance_mode": "维护模式",
     "growi_is_under_maintenance": "GROWI正在进行维护。请等待,直到它结束。",
     "growi_is_under_maintenance": "GROWI正在进行维护。请等待,直到它结束。",

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

@@ -40,7 +40,6 @@ import TrashPageAlert from '../components/Page/TrashPageAlert';
 import PageComment from '../components/PageComment';
 import PageComment from '../components/PageComment';
 import CommentEditorLazyRenderer from '../components/PageComment/CommentEditorLazyRenderer';
 import CommentEditorLazyRenderer from '../components/PageComment/CommentEditorLazyRenderer';
 import PageContentFooter from '../components/PageContentFooter';
 import PageContentFooter from '../components/PageContentFooter';
-import { defaultEditorOptions, defaultPreviewOptions } from '../components/PageEditor/OptionsSelector';
 import BookmarkList from '../components/PageList/BookmarkList';
 import BookmarkList from '../components/PageList/BookmarkList';
 import PageStatusAlert from '../components/PageStatusAlert';
 import PageStatusAlert from '../components/PageStatusAlert';
 import PageTimeline from '../components/PageTimeline';
 import PageTimeline from '../components/PageTimeline';
@@ -66,7 +65,7 @@ const pageContainer = new PageContainer(appContainer);
 const pageHistoryContainer = new PageHistoryContainer(appContainer, pageContainer);
 const pageHistoryContainer = new PageHistoryContainer(appContainer, pageContainer);
 const revisionComparerContainer = new RevisionComparerContainer(appContainer, pageContainer);
 const revisionComparerContainer = new RevisionComparerContainer(appContainer, pageContainer);
 const commentContainer = new CommentContainer(appContainer);
 const commentContainer = new CommentContainer(appContainer);
-const editorContainer = new EditorContainer(appContainer, defaultEditorOptions, defaultPreviewOptions);
+const editorContainer = new EditorContainer(appContainer);
 const tagContainer = new TagContainer(appContainer);
 const tagContainer = new TagContainer(appContainer);
 const personalContainer = new PersonalContainer(appContainer);
 const personalContainer = new PersonalContainer(appContainer);
 const injectableContainers = [
 const injectableContainers = [

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

@@ -1,22 +1,22 @@
 import React from 'react';
 import React from 'react';
 
 
+import AppContainer from '~/client/services/AppContainer';
+import SocketIoContainer from '~/client/services/SocketIoContainer';
+import { DescendantsPageListModal } from '~/components/DescendantsPageListModal';
+import PutbackPageModal from '~/components/PutbackPageModal';
 import Xss from '~/services/xss';
 import Xss from '~/services/xss';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
+import EmptyTrashModal from '../components/EmptyTrashModal';
+import HotkeysManager from '../components/Hotkeys/HotkeysManager';
 import GrowiNavbar from '../components/Navbar/GrowiNavbar';
 import GrowiNavbar from '../components/Navbar/GrowiNavbar';
 import GrowiNavbarBottom from '../components/Navbar/GrowiNavbarBottom';
 import GrowiNavbarBottom from '../components/Navbar/GrowiNavbarBottom';
-import HotkeysManager from '../components/Hotkeys/HotkeysManager';
+import PageAccessoriesModal from '../components/PageAccessoriesModal';
 import PageCreateModal from '../components/PageCreateModal';
 import PageCreateModal from '../components/PageCreateModal';
 import PageDeleteModal from '../components/PageDeleteModal';
 import PageDeleteModal from '../components/PageDeleteModal';
 import PageDuplicateModal from '../components/PageDuplicateModal';
 import PageDuplicateModal from '../components/PageDuplicateModal';
-import PageRenameModal from '../components/PageRenameModal';
 import PagePresentationModal from '../components/PagePresentationModal';
 import PagePresentationModal from '../components/PagePresentationModal';
-import PageAccessoriesModal from '../components/PageAccessoriesModal';
-import PutbackPageModal from '~/components/PutbackPageModal';
-
-import AppContainer from '~/client/services/AppContainer';
-import SocketIoContainer from '~/client/services/SocketIoContainer';
-import { DescendantsPageListModal } from '~/components/DescendantsPageListModal';
+import PageRenameModal from '../components/PageRenameModal';
 
 
 const logger = loggerFactory('growi:cli:app');
 const logger = loggerFactory('growi:cli:app');
 
 
@@ -48,6 +48,7 @@ const componentMappings = {
 
 
   'page-create-modal': <PageCreateModal />,
   'page-create-modal': <PageCreateModal />,
   'page-delete-modal': <PageDeleteModal />,
   'page-delete-modal': <PageDeleteModal />,
+  'empty-trash-modal': <EmptyTrashModal />,
   'page-duplicate-modal': <PageDuplicateModal />,
   'page-duplicate-modal': <PageDuplicateModal />,
   'page-rename-modal': <PageRenameModal />,
   'page-rename-modal': <PageRenameModal />,
   'page-presentation-modal': <PagePresentationModal />,
   'page-presentation-modal': <PagePresentationModal />,

+ 14 - 13
packages/app/src/client/services/AdminAppContainer.js

@@ -1,15 +1,16 @@
 import { Container } from 'unstated';
 import { Container } from 'unstated';
 
 
+import { apiv3Get, apiv3Post, apiv3Put } from '../util/apiv3-client';
+
 /**
 /**
  * Service container for admin app setting page (AppSettings.jsx)
  * Service container for admin app setting page (AppSettings.jsx)
  * @extends {Container} unstated Container
  * @extends {Container} unstated Container
  */
  */
 export default class AdminAppContainer extends Container {
 export default class AdminAppContainer extends Container {
 
 
-  constructor(appContainer) {
+  constructor() {
     super();
     super();
 
 
-    this.appContainer = appContainer;
     this.dummyTitle = 0;
     this.dummyTitle = 0;
     this.dummyTitleForError = 1;
     this.dummyTitleForError = 1;
 
 
@@ -75,7 +76,7 @@ export default class AdminAppContainer extends Container {
    * retrieve app sttings data
    * retrieve app sttings data
    */
    */
   async retrieveAppSettingsData() {
   async retrieveAppSettingsData() {
-    const response = await this.appContainer.apiv3.get('/app-settings/');
+    const response = await apiv3Get('/app-settings/');
     const { appSettingsParams } = response.data;
     const { appSettingsParams } = response.data;
 
 
     this.setState({
     this.setState({
@@ -326,7 +327,7 @@ export default class AdminAppContainer extends Container {
    * @return {Array} Appearance
    * @return {Array} Appearance
    */
    */
   async updateAppSettingHandler() {
   async updateAppSettingHandler() {
-    const response = await this.appContainer.apiv3.put('/app-settings/app-setting', {
+    const response = await apiv3Put('/app-settings/app-setting', {
       title: this.state.title,
       title: this.state.title,
       confidential: this.state.confidential,
       confidential: this.state.confidential,
       globalLang: this.state.globalLang,
       globalLang: this.state.globalLang,
@@ -344,7 +345,7 @@ export default class AdminAppContainer extends Container {
    * @return {Array} Appearance
    * @return {Array} Appearance
    */
    */
   async updateSiteUrlSettingHandler() {
   async updateSiteUrlSettingHandler() {
-    const response = await this.appContainer.apiv3.put('/app-settings/site-url-setting', {
+    const response = await apiv3Put('/app-settings/site-url-setting', {
       siteUrl: this.state.siteUrl,
       siteUrl: this.state.siteUrl,
     });
     });
     const { siteUrlSettingParams } = response.data;
     const { siteUrlSettingParams } = response.data;
@@ -369,7 +370,7 @@ export default class AdminAppContainer extends Container {
    * @return {Array} Appearance
    * @return {Array} Appearance
    */
    */
   async updateSmtpSetting() {
   async updateSmtpSetting() {
-    const response = await this.appContainer.apiv3.put('/app-settings/smtp-setting', {
+    const response = await apiv3Put('/app-settings/smtp-setting', {
       fromAddress: this.state.fromAddress,
       fromAddress: this.state.fromAddress,
       transmissionMethod: this.state.transmissionMethod,
       transmissionMethod: this.state.transmissionMethod,
       smtpHost: this.state.smtpHost,
       smtpHost: this.state.smtpHost,
@@ -388,7 +389,7 @@ export default class AdminAppContainer extends Container {
    * @return {Array} Appearance
    * @return {Array} Appearance
    */
    */
   async updateSesSetting() {
   async updateSesSetting() {
-    const response = await this.appContainer.apiv3.put('/app-settings/ses-setting', {
+    const response = await apiv3Put('/app-settings/ses-setting', {
       fromAddress: this.state.fromAddress,
       fromAddress: this.state.fromAddress,
       transmissionMethod: this.state.transmissionMethod,
       transmissionMethod: this.state.transmissionMethod,
       sesAccessKeyId: this.state.sesAccessKeyId,
       sesAccessKeyId: this.state.sesAccessKeyId,
@@ -404,7 +405,7 @@ export default class AdminAppContainer extends Container {
    * @memberOf AdminAppContainer
    * @memberOf AdminAppContainer
    */
    */
   async sendTestEmail() {
   async sendTestEmail() {
-    return this.appContainer.apiv3.post('/app-settings/smtp-test');
+    return apiv3Post('/app-settings/smtp-test');
   }
   }
 
 
   /**
   /**
@@ -434,7 +435,7 @@ export default class AdminAppContainer extends Container {
       requestParams.s3ReferenceFileWithRelayMode = this.state.s3ReferenceFileWithRelayMode;
       requestParams.s3ReferenceFileWithRelayMode = this.state.s3ReferenceFileWithRelayMode;
     }
     }
 
 
-    const response = await this.appContainer.apiv3.put('/app-settings/file-upload-setting', requestParams);
+    const response = await apiv3Put('/app-settings/file-upload-setting', requestParams);
     const { responseParams } = response.data;
     const { responseParams } = response.data;
     return this.setState(responseParams);
     return this.setState(responseParams);
   }
   }
@@ -445,7 +446,7 @@ export default class AdminAppContainer extends Container {
    * @return {Array} Appearance
    * @return {Array} Appearance
    */
    */
   async updatePluginSettingHandler() {
   async updatePluginSettingHandler() {
-    const response = await this.appContainer.apiv3.put('/app-settings/plugin-setting', {
+    const response = await apiv3Put('/app-settings/plugin-setting', {
       isEnabledPlugins: this.state.isEnabledPlugins,
       isEnabledPlugins: this.state.isEnabledPlugins,
     });
     });
     const { pluginSettingParams } = response.data;
     const { pluginSettingParams } = response.data;
@@ -457,17 +458,17 @@ export default class AdminAppContainer extends Container {
    * @memberOf AdminAppContainer
    * @memberOf AdminAppContainer
    */
    */
   async v5PageMigrationHandler() {
   async v5PageMigrationHandler() {
-    const response = await this.appContainer.apiv3.post('/app-settings/v5-schema-migration');
+    const response = await apiv3Post('/app-settings/v5-schema-migration');
     const { isV5Compatible } = response.data;
     const { isV5Compatible } = response.data;
     return { isV5Compatible };
     return { isV5Compatible };
   }
   }
 
 
   async startMaintenanceMode() {
   async startMaintenanceMode() {
-    await this.appContainer.apiv3.post('/app-settings/maintenance-mode', { flag: true });
+    await apiv3Post('/app-settings/maintenance-mode', { flag: true });
   }
   }
 
 
   async endMaintenanceMode() {
   async endMaintenanceMode() {
-    await this.appContainer.apiv3.post('/app-settings/maintenance-mode', { flag: false });
+    await apiv3Post('/app-settings/maintenance-mode', { flag: false });
   }
   }
 
 
 }
 }

+ 6 - 5
packages/app/src/client/services/AdminBasicSecurityContainer.js

@@ -1,8 +1,10 @@
 import { Container } from 'unstated';
 import { Container } from 'unstated';
-import loggerFactory from '~/utils/logger';
 
 
+import loggerFactory from '~/utils/logger';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';
 
 
+import { apiv3Get, apiv3Put } from '../util/apiv3-client';
+
 const logger = loggerFactory('growi:security:AdminTwitterSecurityContainer');
 const logger = loggerFactory('growi:security:AdminTwitterSecurityContainer');
 
 
 /**
 /**
@@ -11,10 +13,9 @@ const logger = loggerFactory('growi:security:AdminTwitterSecurityContainer');
  */
  */
 export default class AdminBasicSecurityContainer extends Container {
 export default class AdminBasicSecurityContainer extends Container {
 
 
-  constructor(appContainer) {
+  constructor() {
     super();
     super();
 
 
-    this.appContainer = appContainer;
     this.dummyIsSameUsernameTreatedAsIdenticalUser = 0;
     this.dummyIsSameUsernameTreatedAsIdenticalUser = 0;
     this.dummyIsSameUsernameTreatedAsIdenticalUserForError = 1;
     this.dummyIsSameUsernameTreatedAsIdenticalUserForError = 1;
 
 
@@ -31,7 +32,7 @@ export default class AdminBasicSecurityContainer extends Container {
    */
    */
   async retrieveSecurityData() {
   async retrieveSecurityData() {
     try {
     try {
-      const response = await this.appContainer.apiv3.get('/security-setting/');
+      const response = await apiv3Get('/security-setting/');
       const { basicAuth } = response.data.securityParams;
       const { basicAuth } = response.data.securityParams;
       this.setState({
       this.setState({
         isSameUsernameTreatedAsIdenticalUser: basicAuth.isSameUsernameTreatedAsIdenticalUser,
         isSameUsernameTreatedAsIdenticalUser: basicAuth.isSameUsernameTreatedAsIdenticalUser,
@@ -65,7 +66,7 @@ export default class AdminBasicSecurityContainer extends Container {
     let requestParams = { isSameUsernameTreatedAsIdenticalUser: this.state.isSameUsernameTreatedAsIdenticalUser };
     let requestParams = { isSameUsernameTreatedAsIdenticalUser: this.state.isSameUsernameTreatedAsIdenticalUser };
 
 
     requestParams = await removeNullPropertyFromObject(requestParams);
     requestParams = await removeNullPropertyFromObject(requestParams);
-    const response = await this.appContainer.apiv3.put('/security-setting/basic', requestParams);
+    const response = await apiv3Put('/security-setting/basic', requestParams);
     const { securitySettingParams } = response.data;
     const { securitySettingParams } = response.data;
 
 
     this.setState({
     this.setState({

+ 11 - 11
packages/app/src/client/services/AdminCustomizeContainer.js

@@ -3,6 +3,7 @@ import { Container } from 'unstated';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { toastError } from '../util/apiNotification';
 import { toastError } from '../util/apiNotification';
+import { apiv3Get, apiv3Put } from '../util/apiv3-client';
 
 
 // eslint-disable-next-line no-unused-vars
 // eslint-disable-next-line no-unused-vars
 const logger = loggerFactory('growi:services:AdminCustomizeContainer');
 const logger = loggerFactory('growi:services:AdminCustomizeContainer');
@@ -13,10 +14,9 @@ const logger = loggerFactory('growi:services:AdminCustomizeContainer');
  */
  */
 export default class AdminCustomizeContainer extends Container {
 export default class AdminCustomizeContainer extends Container {
 
 
-  constructor(appContainer) {
+  constructor() {
     super();
     super();
 
 
-    this.appContainer = appContainer;
     this.dummyCurrentTheme = 0;
     this.dummyCurrentTheme = 0;
     this.dummyCurrentThemeForError = 1;
     this.dummyCurrentThemeForError = 1;
 
 
@@ -76,7 +76,7 @@ export default class AdminCustomizeContainer extends Container {
    */
    */
   async retrieveCustomizeData() {
   async retrieveCustomizeData() {
     try {
     try {
-      const response = await this.appContainer.apiv3.get('/customize-setting/');
+      const response = await apiv3Get('/customize-setting/');
       const { customizeParams } = response.data;
       const { customizeParams } = response.data;
 
 
       this.setState({
       this.setState({
@@ -246,7 +246,7 @@ export default class AdminCustomizeContainer extends Container {
   async previewTheme(themeName) {
   async previewTheme(themeName) {
     try {
     try {
       // get theme asset path
       // get theme asset path
-      const response = await this.appContainer.apiv3.get('/customize-setting/theme/asset-path', { themeName });
+      const response = await apiv3Get('/customize-setting/theme/asset-path', { themeName });
       const { assetPath } = response.data;
       const { assetPath } = response.data;
 
 
       const themeLink = document.getElementById('grw-theme-link');
       const themeLink = document.getElementById('grw-theme-link');
@@ -274,7 +274,7 @@ export default class AdminCustomizeContainer extends Container {
    */
    */
   async updateCustomizeTheme() {
   async updateCustomizeTheme() {
     try {
     try {
-      const response = await this.appContainer.apiv3.put('/customize-setting/theme', {
+      const response = await apiv3Put('/customize-setting/theme', {
         themeType: this.state.currentTheme,
         themeType: this.state.currentTheme,
       });
       });
       const { customizedParams } = response.data;
       const { customizedParams } = response.data;
@@ -294,7 +294,7 @@ export default class AdminCustomizeContainer extends Container {
    */
    */
   async updateCustomizeFunction() {
   async updateCustomizeFunction() {
     try {
     try {
-      const response = await this.appContainer.apiv3.put('/customize-setting/function', {
+      const response = await apiv3Put('/customize-setting/function', {
         isEnabledTimeline: this.state.isEnabledTimeline,
         isEnabledTimeline: this.state.isEnabledTimeline,
         isSavedStatesOfTabChanges: this.state.isSavedStatesOfTabChanges,
         isSavedStatesOfTabChanges: this.state.isSavedStatesOfTabChanges,
         isEnabledAttachTitleHeader: this.state.isEnabledAttachTitleHeader,
         isEnabledAttachTitleHeader: this.state.isEnabledAttachTitleHeader,
@@ -332,7 +332,7 @@ export default class AdminCustomizeContainer extends Container {
    */
    */
   async updateHighlightJsStyle() {
   async updateHighlightJsStyle() {
     try {
     try {
-      const response = await this.appContainer.apiv3.put('/customize-setting/highlight', {
+      const response = await apiv3Put('/customize-setting/highlight', {
         highlightJsStyle: this.state.currentHighlightJsStyleId,
         highlightJsStyle: this.state.currentHighlightJsStyleId,
         highlightJsStyleBorder: this.state.isHighlightJsStyleBorderEnabled,
         highlightJsStyleBorder: this.state.isHighlightJsStyleBorderEnabled,
       });
       });
@@ -354,7 +354,7 @@ export default class AdminCustomizeContainer extends Container {
    */
    */
   async updateCustomizeTitle() {
   async updateCustomizeTitle() {
     try {
     try {
-      const response = await this.appContainer.apiv3.put('/customize-setting/customize-title', {
+      const response = await apiv3Put('/customize-setting/customize-title', {
         customizeTitle: this.state.currentCustomizeTitle,
         customizeTitle: this.state.currentCustomizeTitle,
       });
       });
       const { customizedParams } = response.data;
       const { customizedParams } = response.data;
@@ -374,7 +374,7 @@ export default class AdminCustomizeContainer extends Container {
    */
    */
   async updateCustomizeHeader() {
   async updateCustomizeHeader() {
     try {
     try {
-      const response = await this.appContainer.apiv3.put('/customize-setting/customize-header', {
+      const response = await apiv3Put('/customize-setting/customize-header', {
         customizeHeader: this.state.currentCustomizeHeader,
         customizeHeader: this.state.currentCustomizeHeader,
       });
       });
       const { customizedParams } = response.data;
       const { customizedParams } = response.data;
@@ -394,7 +394,7 @@ export default class AdminCustomizeContainer extends Container {
    */
    */
   async updateCustomizeCss() {
   async updateCustomizeCss() {
     try {
     try {
-      const response = await this.appContainer.apiv3.put('/customize-setting/customize-css', {
+      const response = await apiv3Put('/customize-setting/customize-css', {
         customizeCss: this.state.currentCustomizeCss,
         customizeCss: this.state.currentCustomizeCss,
       });
       });
       const { customizedParams } = response.data;
       const { customizedParams } = response.data;
@@ -415,7 +415,7 @@ export default class AdminCustomizeContainer extends Container {
    */
    */
   async updateCustomizeScript() {
   async updateCustomizeScript() {
     try {
     try {
-      const response = await this.appContainer.apiv3.put('/customize-setting/customize-script', {
+      const response = await apiv3Put('/customize-setting/customize-script', {
         customizeScript: this.state.currentCustomizeScript,
         customizeScript: this.state.currentCustomizeScript,
       });
       });
       const { customizedParams } = response.data;
       const { customizedParams } = response.data;

+ 5 - 5
packages/app/src/client/services/AdminExternalAccountsContainer.js

@@ -2,6 +2,8 @@ import { Container } from 'unstated';
 
 
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
+import { apiv3Delete, apiv3Get } from '../util/apiv3-client';
+
 
 
 // eslint-disable-next-line no-unused-vars
 // eslint-disable-next-line no-unused-vars
 const logger = loggerFactory('growi:services:AdminexternalaccountsContainer');
 const logger = loggerFactory('growi:services:AdminexternalaccountsContainer');
@@ -12,11 +14,9 @@ const logger = loggerFactory('growi:services:AdminexternalaccountsContainer');
  */
  */
 export default class AdminExternalAccountsContainer extends Container {
 export default class AdminExternalAccountsContainer extends Container {
 
 
-  constructor(appContainer) {
+  constructor() {
     super();
     super();
 
 
-    this.appContainer = appContainer;
-
     this.state = {
     this.state = {
       externalAccounts: [],
       externalAccounts: [],
       totalAccounts: 0,
       totalAccounts: 0,
@@ -42,7 +42,7 @@ export default class AdminExternalAccountsContainer extends Container {
   async retrieveExternalAccountsByPagingNum(selectedPage) {
   async retrieveExternalAccountsByPagingNum(selectedPage) {
 
 
     const params = { page: selectedPage };
     const params = { page: selectedPage };
-    const { data } = await this.appContainer.apiv3.get('/users/external-accounts', params);
+    const { data } = await apiv3Get('/users/external-accounts', params);
 
 
     if (data.paginateResult == null) {
     if (data.paginateResult == null) {
       throw new Error('data must conclude \'paginateResult\' property.');
       throw new Error('data must conclude \'paginateResult\' property.');
@@ -64,7 +64,7 @@ export default class AdminExternalAccountsContainer extends Container {
    * @param {string} externalAccountId id of the External Account to be removed
    * @param {string} externalAccountId id of the External Account to be removed
    */
    */
   async removeExternalAccountById(externalAccountId) {
   async removeExternalAccountById(externalAccountId) {
-    const res = await this.appContainer.apiv3.delete(`/users/external-accounts/${externalAccountId}/remove`);
+    const res = await apiv3Delete(`/users/external-accounts/${externalAccountId}/remove`);
     const deletedUserData = res.data.externalAccount;
     const deletedUserData = res.data.externalAccount;
     await this.retrieveExternalAccountsByPagingNum(this.state.activePage);
     await this.retrieveExternalAccountsByPagingNum(this.state.activePage);
     return deletedUserData.accountId;
     return deletedUserData.accountId;

+ 9 - 8
packages/app/src/client/services/AdminGeneralSecurityContainer.js

@@ -4,9 +4,11 @@ import {
   PageSingleDeleteConfigValue, PageSingleDeleteCompConfigValue,
   PageSingleDeleteConfigValue, PageSingleDeleteCompConfigValue,
   PageRecursiveDeleteConfigValue, PageRecursiveDeleteCompConfigValue,
   PageRecursiveDeleteConfigValue, PageRecursiveDeleteCompConfigValue,
 } from '~/interfaces/page-delete-config';
 } from '~/interfaces/page-delete-config';
-import { toastError } from '../util/apiNotification';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';
 
 
+import { toastError } from '../util/apiNotification';
+import { apiv3Get, apiv3Put } from '../util/apiv3-client';
+
 /**
 /**
  * Service container for admin security page (SecuritySetting.jsx)
  * Service container for admin security page (SecuritySetting.jsx)
  * @extends {Container} unstated Container
  * @extends {Container} unstated Container
@@ -16,7 +18,6 @@ export default class AdminGeneralSecurityContainer extends Container {
   constructor(appContainer) {
   constructor(appContainer) {
     super();
     super();
 
 
-    this.appContainer = appContainer;
     this.dummyCurrentRestrictGuestMode = 0;
     this.dummyCurrentRestrictGuestMode = 0;
     this.dummyCurrentRestrictGuestModeForError = 1;
     this.dummyCurrentRestrictGuestModeForError = 1;
 
 
@@ -64,7 +65,7 @@ export default class AdminGeneralSecurityContainer extends Container {
 
 
   async retrieveSecurityData() {
   async retrieveSecurityData() {
     await this.retrieveSetupStratedies();
     await this.retrieveSetupStratedies();
-    const response = await this.appContainer.apiv3.get('/security-setting/');
+    const response = await apiv3Get('/security-setting/');
     const { generalSetting, shareLinkSetting, generalAuth } = response.data.securityParams;
     const { generalSetting, shareLinkSetting, generalAuth } = response.data.securityParams;
     this.setState({
     this.setState({
       currentRestrictGuestMode: generalSetting.restrictGuestMode,
       currentRestrictGuestMode: generalSetting.restrictGuestMode,
@@ -215,7 +216,7 @@ export default class AdminGeneralSecurityContainer extends Container {
     };
     };
 
 
     requestParams = await removeNullPropertyFromObject(requestParams);
     requestParams = await removeNullPropertyFromObject(requestParams);
-    const response = await this.appContainer.apiv3.put('/security-setting/general-setting', requestParams);
+    const response = await apiv3Put('/security-setting/general-setting', requestParams);
     const { securitySettingParams } = response.data;
     const { securitySettingParams } = response.data;
     return securitySettingParams;
     return securitySettingParams;
   }
   }
@@ -227,7 +228,7 @@ export default class AdminGeneralSecurityContainer extends Container {
     const requestParams = {
     const requestParams = {
       disableLinkSharing: !this.state.disableLinkSharing,
       disableLinkSharing: !this.state.disableLinkSharing,
     };
     };
-    const response = await this.appContainer.apiv3.put('/security-setting/share-link-setting', requestParams);
+    const response = await apiv3Put('/security-setting/share-link-setting', requestParams);
     this.setDisableLinkSharing(!this.state.disableLinkSharing);
     this.setDisableLinkSharing(!this.state.disableLinkSharing);
     return response;
     return response;
   }
   }
@@ -238,7 +239,7 @@ export default class AdminGeneralSecurityContainer extends Container {
   async switchAuthentication(stateVariableName, authId) {
   async switchAuthentication(stateVariableName, authId) {
     const isEnabled = !this.state[stateVariableName];
     const isEnabled = !this.state[stateVariableName];
     try {
     try {
-      await this.appContainer.apiv3.put('/security-setting/authentication/enabled', {
+      await apiv3Put('/security-setting/authentication/enabled', {
         isEnabled,
         isEnabled,
         authId,
         authId,
       });
       });
@@ -255,7 +256,7 @@ export default class AdminGeneralSecurityContainer extends Container {
    */
    */
   async retrieveSetupStratedies() {
   async retrieveSetupStratedies() {
     try {
     try {
-      const response = await this.appContainer.apiv3.get('/security-setting/authentication');
+      const response = await apiv3Get('/security-setting/authentication');
       const { setupStrategies } = response.data;
       const { setupStrategies } = response.data;
       this.setState({ setupStrategies });
       this.setState({ setupStrategies });
     }
     }
@@ -273,7 +274,7 @@ export default class AdminGeneralSecurityContainer extends Container {
       page,
       page,
     };
     };
 
 
-    const { data } = await this.appContainer.apiv3.get('/security-setting/all-share-links', params);
+    const { data } = await apiv3Get('/security-setting/all-share-links', params);
 
 
     if (data.paginateResult == null) {
     if (data.paginateResult == null) {
       throw new Error('data must conclude \'paginateResult\' property.');
       throw new Error('data must conclude \'paginateResult\' property.');

+ 6 - 5
packages/app/src/client/services/AdminGitHubSecurityContainer.js

@@ -1,10 +1,12 @@
-import { Container } from 'unstated';
-
 import { pathUtils } from '@growi/core';
 import { pathUtils } from '@growi/core';
+import { Container } from 'unstated';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
+
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';
 
 
+import { apiv3Get, apiv3Put } from '../util/apiv3-client';
+
 const logger = loggerFactory('growi:security:AdminGitHubSecurityContainer');
 const logger = loggerFactory('growi:security:AdminGitHubSecurityContainer');
 
 
 /**
 /**
@@ -16,7 +18,6 @@ export default class AdminGitHubSecurityContainer extends Container {
   constructor(appContainer) {
   constructor(appContainer) {
     super();
     super();
 
 
-    this.appContainer = appContainer;
     this.dummyGithubClientId = 0;
     this.dummyGithubClientId = 0;
     this.dummyGithubClientIdForError = 1;
     this.dummyGithubClientIdForError = 1;
 
 
@@ -36,7 +37,7 @@ export default class AdminGitHubSecurityContainer extends Container {
    */
    */
   async retrieveSecurityData() {
   async retrieveSecurityData() {
     try {
     try {
-      const response = await this.appContainer.apiv3.get('/security-setting/');
+      const response = await apiv3Get('/security-setting/');
       const { githubOAuth } = response.data.securityParams;
       const { githubOAuth } = response.data.securityParams;
       this.setState({
       this.setState({
         githubClientId: githubOAuth.githubClientId,
         githubClientId: githubOAuth.githubClientId,
@@ -88,7 +89,7 @@ export default class AdminGitHubSecurityContainer extends Container {
     let requestParams = { githubClientId, githubClientSecret, isSameUsernameTreatedAsIdenticalUser };
     let requestParams = { githubClientId, githubClientSecret, isSameUsernameTreatedAsIdenticalUser };
 
 
     requestParams = await removeNullPropertyFromObject(requestParams);
     requestParams = await removeNullPropertyFromObject(requestParams);
-    const response = await this.appContainer.apiv3.put('/security-setting/github-oauth', requestParams);
+    const response = await apiv3Put('/security-setting/github-oauth', requestParams);
     const { securitySettingParams } = response.data;
     const { securitySettingParams } = response.data;
 
 
     this.setState({
     this.setState({

+ 16 - 13
packages/app/src/client/services/AdminGoogleSecurityContainer.js

@@ -1,10 +1,12 @@
-import { Container } from 'unstated';
-
 import { pathUtils } from '@growi/core';
 import { pathUtils } from '@growi/core';
+import { Container } from 'unstated';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
+
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';
 
 
+import { apiv3Get, apiv3Put } from '../util/apiv3-client';
+
 const logger = loggerFactory('growi:security:AdminGoogleSecurityContainer');
 const logger = loggerFactory('growi:security:AdminGoogleSecurityContainer');
 
 
 /**
 /**
@@ -16,7 +18,6 @@ export default class AdminGoogleSecurityContainer extends Container {
   constructor(appContainer) {
   constructor(appContainer) {
     super();
     super();
 
 
-    this.appContainer = appContainer;
     this.dummyGoogleClientId = 0;
     this.dummyGoogleClientId = 0;
     this.dummyGoogleClientIdForError = 1;
     this.dummyGoogleClientIdForError = 1;
 
 
@@ -26,7 +27,7 @@ export default class AdminGoogleSecurityContainer extends Container {
       // set dummy value tile for using suspense
       // set dummy value tile for using suspense
       googleClientId: this.dummyGoogleClientId,
       googleClientId: this.dummyGoogleClientId,
       googleClientSecret: '',
       googleClientSecret: '',
-      isSameUsernameTreatedAsIdenticalUser: false,
+      isSameEmailTreatedAsIdenticalUser: false,
     };
     };
 
 
 
 
@@ -37,12 +38,12 @@ export default class AdminGoogleSecurityContainer extends Container {
    */
    */
   async retrieveSecurityData() {
   async retrieveSecurityData() {
     try {
     try {
-      const response = await this.appContainer.apiv3.get('/security-setting/');
+      const response = await apiv3Get('/security-setting/');
       const { googleOAuth } = response.data.securityParams;
       const { googleOAuth } = response.data.securityParams;
       this.setState({
       this.setState({
         googleClientId: googleOAuth.googleClientId,
         googleClientId: googleOAuth.googleClientId,
         googleClientSecret: googleOAuth.googleClientSecret,
         googleClientSecret: googleOAuth.googleClientSecret,
-        isSameUsernameTreatedAsIdenticalUser: googleOAuth.isSameUsernameTreatedAsIdenticalUser,
+        isSameEmailTreatedAsIdenticalUser: googleOAuth.isSameEmailTreatedAsIdenticalUser,
       });
       });
     }
     }
     catch (err) {
     catch (err) {
@@ -74,30 +75,32 @@ export default class AdminGoogleSecurityContainer extends Container {
   }
   }
 
 
   /**
   /**
-   * Switch isSameUsernameTreatedAsIdenticalUser
+   * Switch isSameEmailTreatedAsIdenticalUser
    */
    */
-  switchIsSameUsernameTreatedAsIdenticalUser() {
-    this.setState({ isSameUsernameTreatedAsIdenticalUser: !this.state.isSameUsernameTreatedAsIdenticalUser });
+  switchIsSameEmailTreatedAsIdenticalUser() {
+    this.setState({ isSameEmailTreatedAsIdenticalUser: !this.state.isSameEmailTreatedAsIdenticalUser });
   }
   }
 
 
+
   /**
   /**
    * Update googleSetting
    * Update googleSetting
    */
    */
   async updateGoogleSetting() {
   async updateGoogleSetting() {
-    const { googleClientId, googleClientSecret, isSameUsernameTreatedAsIdenticalUser } = this.state;
+    const { googleClientId, googleClientSecret, isSameEmailTreatedAsIdenticalUser } = this.state;
+    console.log('updateGoogleSetting', isSameEmailTreatedAsIdenticalUser);
 
 
     let requestParams = {
     let requestParams = {
-      googleClientId, googleClientSecret, isSameUsernameTreatedAsIdenticalUser,
+      googleClientId, googleClientSecret, isSameEmailTreatedAsIdenticalUser,
     };
     };
 
 
     requestParams = await removeNullPropertyFromObject(requestParams);
     requestParams = await removeNullPropertyFromObject(requestParams);
-    const response = await this.appContainer.apiv3.put('/security-setting/google-oauth', requestParams);
+    const response = await apiv3Put('/security-setting/google-oauth', requestParams);
     const { securitySettingParams } = response.data;
     const { securitySettingParams } = response.data;
 
 
     this.setState({
     this.setState({
       googleClientId: securitySettingParams.googleClientId,
       googleClientId: securitySettingParams.googleClientId,
       googleClientSecret: securitySettingParams.googleClientSecret,
       googleClientSecret: securitySettingParams.googleClientSecret,
-      isSameUsernameTreatedAsIdenticalUser: securitySettingParams.isSameUsernameTreatedAsIdenticalUser,
+      isSameEmailTreatedAsIdenticalUser: securitySettingParams.isSameEmailTreatedAsIdenticalUser,
     });
     });
     return response;
     return response;
   }
   }

+ 3 - 4
packages/app/src/client/services/AdminHomeContainer.js

@@ -3,6 +3,7 @@ import { Container } from 'unstated';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { toastError } from '../util/apiNotification';
 import { toastError } from '../util/apiNotification';
+import { apiv3Get } from '../util/apiv3-client';
 
 
 // eslint-disable-next-line no-unused-vars
 // eslint-disable-next-line no-unused-vars
 const logger = loggerFactory('growi:services:AdminHomeContainer');
 const logger = loggerFactory('growi:services:AdminHomeContainer');
@@ -13,11 +14,9 @@ const logger = loggerFactory('growi:services:AdminHomeContainer');
  */
  */
 export default class AdminHomeContainer extends Container {
 export default class AdminHomeContainer extends Container {
 
 
-  constructor(appContainer) {
+  constructor() {
     super();
     super();
 
 
-    this.appContainer = appContainer;
-
     this.copyStateValues = {
     this.copyStateValues = {
       DEFAULT: 'default',
       DEFAULT: 'default',
       DONE: 'done',
       DONE: 'done',
@@ -53,7 +52,7 @@ export default class AdminHomeContainer extends Container {
    */
    */
   async retrieveAdminHomeData() {
   async retrieveAdminHomeData() {
     try {
     try {
-      const response = await this.appContainer.apiv3.get('/admin-home/');
+      const response = await apiv3Get('/admin-home/');
       const { adminHomeParams } = response.data;
       const { adminHomeParams } = response.data;
 
 
       this.setState(prevState => ({
       this.setState(prevState => ({

+ 9 - 7
packages/app/src/client/services/AdminImportContainer.js

@@ -3,6 +3,8 @@ import { Container } from 'unstated';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { toastSuccess, toastError } from '../util/apiNotification';
 import { toastSuccess, toastError } from '../util/apiNotification';
+import { apiPost } from '../util/apiv1-client';
+import { apiv3Get } from '../util/apiv3-client';
 
 
 const logger = loggerFactory('growi:appSettings');
 const logger = loggerFactory('growi:appSettings');
 
 
@@ -48,7 +50,7 @@ export default class AdminImportContainer extends Container {
    * retrieve app sttings data
    * retrieve app sttings data
    */
    */
   async retrieveImportSettingsData() {
   async retrieveImportSettingsData() {
-    const response = await this.appContainer.apiv3.get('/import/');
+    const response = await apiv3Get('/import/');
     const {
     const {
       importSettingsParams,
       importSettingsParams,
     } = response.data;
     } = response.data;
@@ -73,7 +75,7 @@ export default class AdminImportContainer extends Container {
         'importer:esa:team_name': this.state.esaTeamName,
         'importer:esa:team_name': this.state.esaTeamName,
         'importer:esa:access_token': this.state.esaAccessToken,
         'importer:esa:access_token': this.state.esaAccessToken,
       };
       };
-      await this.appContainer.apiPost('/admin/import/esa', params);
+      await apiPost('/admin/import/esa', params);
       toastSuccess('Import posts from esa success.');
       toastSuccess('Import posts from esa success.');
     }
     }
     catch (err) {
     catch (err) {
@@ -88,7 +90,7 @@ export default class AdminImportContainer extends Container {
         'importer:esa:team_name': this.state.esaTeamName,
         'importer:esa:team_name': this.state.esaTeamName,
         'importer:esa:access_token': this.state.esaAccessToken,
         'importer:esa:access_token': this.state.esaAccessToken,
       };
       };
-      await this.appContainer.apiPost('/admin/import/testEsaAPI', params);
+      await apiPost('/admin/import/testEsaAPI', params);
       toastSuccess('Test connection to esa success.');
       toastSuccess('Test connection to esa success.');
     }
     }
     catch (error) {
     catch (error) {
@@ -102,7 +104,7 @@ export default class AdminImportContainer extends Container {
       'importer:esa:access_token': this.state.esaAccessToken,
       'importer:esa:access_token': this.state.esaAccessToken,
     };
     };
     try {
     try {
-      await this.appContainer.apiPost('/admin/settings/importerEsa', params);
+      await apiPost('/admin/settings/importerEsa', params);
       toastSuccess('Updated');
       toastSuccess('Updated');
     }
     }
     catch (err) {
     catch (err) {
@@ -117,7 +119,7 @@ export default class AdminImportContainer extends Container {
         'importer:qiita:team_name': this.state.qiitaTeamName,
         'importer:qiita:team_name': this.state.qiitaTeamName,
         'importer:qiita:access_token': this.state.qiitaAccessToken,
         'importer:qiita:access_token': this.state.qiitaAccessToken,
       };
       };
-      await this.appContainer.apiPost('/admin/import/qiita', params);
+      await apiPost('/admin/import/qiita', params);
       toastSuccess('Import posts from qiita:team success.');
       toastSuccess('Import posts from qiita:team success.');
     }
     }
     catch (err) {
     catch (err) {
@@ -133,7 +135,7 @@ export default class AdminImportContainer extends Container {
         'importer:qiita:team_name': this.state.qiitaTeamName,
         'importer:qiita:team_name': this.state.qiitaTeamName,
         'importer:qiita:access_token': this.state.qiitaAccessToken,
         'importer:qiita:access_token': this.state.qiitaAccessToken,
       };
       };
-      await this.appContainer.apiPost('/admin/import/testQiitaAPI', params);
+      await apiPost('/admin/import/testQiitaAPI', params);
       toastSuccess('Test connection to qiita:team success.');
       toastSuccess('Test connection to qiita:team success.');
     }
     }
     catch (err) {
     catch (err) {
@@ -148,7 +150,7 @@ export default class AdminImportContainer extends Container {
       'importer:qiita:access_token': this.state.qiitaAccessToken,
       'importer:qiita:access_token': this.state.qiitaAccessToken,
     };
     };
     try {
     try {
-      await this.appContainer.apiPost('/admin/settings/importerQiita', params);
+      await apiPost('/admin/settings/importerQiita', params);
       toastSuccess('Updated');
       toastSuccess('Updated');
     }
     }
     catch (err) {
     catch (err) {

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

@@ -1,8 +1,10 @@
 import { Container } from 'unstated';
 import { Container } from 'unstated';
-import loggerFactory from '~/utils/logger';
 
 
+import loggerFactory from '~/utils/logger';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';
 
 
+import { apiv3Get, apiv3Put } from '../util/apiv3-client';
+
 const logger = loggerFactory('growi:services:AdminLdapSecurityContainer');
 const logger = loggerFactory('growi:services:AdminLdapSecurityContainer');
 
 
 /**
 /**
@@ -42,7 +44,7 @@ export default class AdminLdapSecurityContainer extends Container {
    */
    */
   async retrieveSecurityData() {
   async retrieveSecurityData() {
     try {
     try {
-      const response = await this.appContainer.apiv3.get('/security-setting/');
+      const response = await apiv3Get('/security-setting/');
       const { ldapAuth } = response.data.securityParams;
       const { ldapAuth } = response.data.securityParams;
       this.setState({
       this.setState({
         serverUrl: ldapAuth.serverUrl,
         serverUrl: ldapAuth.serverUrl,
@@ -183,7 +185,7 @@ export default class AdminLdapSecurityContainer extends Container {
     };
     };
 
 
     requestParams = await removeNullPropertyFromObject(requestParams);
     requestParams = await removeNullPropertyFromObject(requestParams);
-    const response = await this.appContainer.apiv3.put('/security-setting/ldap', requestParams);
+    const response = await apiv3Put('/security-setting/ldap', requestParams);
     const { securitySettingParams } = response.data;
     const { securitySettingParams } = response.data;
 
 
     this.setState({
     this.setState({

+ 5 - 2
packages/app/src/client/services/AdminLocalSecurityContainer.js

@@ -1,6 +1,9 @@
 import { Container } from 'unstated';
 import { Container } from 'unstated';
+
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
+import { apiv3Get, apiv3Put } from '../util/apiv3-client';
+
 // eslint-disable-next-line no-unused-vars
 // eslint-disable-next-line no-unused-vars
 const logger = loggerFactory('growi:services:AdminLocalSecurityContainer');
 const logger = loggerFactory('growi:services:AdminLocalSecurityContainer');
 /**
 /**
@@ -30,7 +33,7 @@ export default class AdminLocalSecurityContainer extends Container {
 
 
   async retrieveSecurityData() {
   async retrieveSecurityData() {
     try {
     try {
-      const response = await this.appContainer.apiv3.get('/security-setting/');
+      const response = await apiv3Get('/security-setting/');
       const { localSetting } = response.data.securityParams;
       const { localSetting } = response.data.securityParams;
       this.setState({
       this.setState({
         useOnlyEnvVars: localSetting.useOnlyEnvVarsForSomeOptions,
         useOnlyEnvVars: localSetting.useOnlyEnvVarsForSomeOptions,
@@ -89,7 +92,7 @@ export default class AdminLocalSecurityContainer extends Container {
    */
    */
   async updateLocalSecuritySetting() {
   async updateLocalSecuritySetting() {
     const { registrationWhiteList, isPasswordResetEnabled, isEmailAuthenticationEnabled } = this.state;
     const { registrationWhiteList, isPasswordResetEnabled, isEmailAuthenticationEnabled } = this.state;
-    const response = await this.appContainer.apiv3.put('/security-setting/local-setting', {
+    const response = await apiv3Put('/security-setting/local-setting', {
       registrationMode: this.state.registrationMode,
       registrationMode: this.state.registrationMode,
       registrationWhiteList,
       registrationWhiteList,
       isPasswordResetEnabled,
       isPasswordResetEnabled,

+ 7 - 5
packages/app/src/client/services/AdminMarkDownContainer.js

@@ -1,5 +1,7 @@
 import { Container } from 'unstated';
 import { Container } from 'unstated';
 
 
+import { apiv3Get, apiv3Put } from '../util/apiv3-client';
+
 /**
 /**
  * Service container for admin markdown setting page (MarkDownSetting.jsx)
  * Service container for admin markdown setting page (MarkDownSetting.jsx)
  * @extends {Container} unstated Container
  * @extends {Container} unstated Container
@@ -43,7 +45,7 @@ export default class AdminMarkDownContainer extends Container {
    * retrieve markdown data
    * retrieve markdown data
    */
    */
   async retrieveMarkdownData() {
   async retrieveMarkdownData() {
-    const response = await this.appContainer.apiv3.get('/markdown-setting/');
+    const response = await apiv3Get('/markdown-setting/');
     const { markdownParams } = response.data;
     const { markdownParams } = response.data;
 
 
     this.setState({
     this.setState({
@@ -93,7 +95,7 @@ export default class AdminMarkDownContainer extends Container {
    */
    */
   async updateLineBreakSetting() {
   async updateLineBreakSetting() {
 
 
-    const response = await this.appContainer.apiv3.put('/markdown-setting/lineBreak', {
+    const response = await apiv3Put('/markdown-setting/lineBreak', {
       isEnabledLinebreaks: this.state.isEnabledLinebreaks,
       isEnabledLinebreaks: this.state.isEnabledLinebreaks,
       isEnabledLinebreaksInComments: this.state.isEnabledLinebreaksInComments,
       isEnabledLinebreaksInComments: this.state.isEnabledLinebreaksInComments,
     });
     });
@@ -106,7 +108,7 @@ export default class AdminMarkDownContainer extends Container {
    */
    */
   async updateIndentSetting() {
   async updateIndentSetting() {
 
 
-    const response = await this.appContainer.apiv3.put('/markdown-setting/indent', {
+    const response = await apiv3Put('/markdown-setting/indent', {
       adminPreferredIndentSize: this.state.adminPreferredIndentSize,
       adminPreferredIndentSize: this.state.adminPreferredIndentSize,
       isIndentSizeForced: this.state.isIndentSizeForced,
       isIndentSizeForced: this.state.isIndentSizeForced,
     });
     });
@@ -123,7 +125,7 @@ export default class AdminMarkDownContainer extends Container {
     tagWhiteList = Array.isArray(tagWhiteList) ? tagWhiteList : tagWhiteList.split(',');
     tagWhiteList = Array.isArray(tagWhiteList) ? tagWhiteList : tagWhiteList.split(',');
     attrWhiteList = Array.isArray(attrWhiteList) ? attrWhiteList : attrWhiteList.split(',');
     attrWhiteList = Array.isArray(attrWhiteList) ? attrWhiteList : attrWhiteList.split(',');
 
 
-    const response = await this.appContainer.apiv3.put('/markdown-setting/xss', {
+    const response = await apiv3Put('/markdown-setting/xss', {
       isEnabledXss: this.state.isEnabledXss,
       isEnabledXss: this.state.isEnabledXss,
       xssOption: this.state.xssOption,
       xssOption: this.state.xssOption,
       tagWhiteList,
       tagWhiteList,
@@ -138,7 +140,7 @@ export default class AdminMarkDownContainer extends Container {
    */
    */
   async updatePresentationSetting() {
   async updatePresentationSetting() {
 
 
-    const response = await this.appContainer.apiv3.put('/markdown-setting/presentation', {
+    const response = await apiv3Put('/markdown-setting/presentation', {
       pageBreakSeparator: this.state.pageBreakSeparator,
       pageBreakSeparator: this.state.pageBreakSeparator,
       pageBreakCustomSeparator: this.state.pageBreakCustomSeparator,
       pageBreakCustomSeparator: this.state.pageBreakCustomSeparator,
     });
     });

+ 10 - 6
packages/app/src/client/services/AdminNotificationContainer.js

@@ -1,5 +1,9 @@
 import { Container } from 'unstated';
 import { Container } from 'unstated';
 
 
+import {
+  apiv3Delete, apiv3Get, apiv3Post, apiv3Put,
+} from '../util/apiv3-client';
+
 /**
 /**
  * Service container for admin Notification setting page (NotificationSetting.jsx)
  * Service container for admin Notification setting page (NotificationSetting.jsx)
  * @extends {Container} unstated Container
  * @extends {Container} unstated Container
@@ -37,7 +41,7 @@ export default class AdminNotificationContainer extends Container {
    * Retrieve notificationData
    * Retrieve notificationData
    */
    */
   async retrieveNotificationData() {
   async retrieveNotificationData() {
-    const response = await this.appContainer.apiv3.get('/notification-setting/');
+    const response = await apiv3Get('/notification-setting/');
     const { notificationParams } = response.data;
     const { notificationParams } = response.data;
 
 
     this.setState({
     this.setState({
@@ -57,7 +61,7 @@ export default class AdminNotificationContainer extends Container {
    * @memberOf SlackAppConfiguration
    * @memberOf SlackAppConfiguration
    */
    */
   async updateSlackAppConfiguration() {
   async updateSlackAppConfiguration() {
-    const response = await this.appContainer.apiv3.put('/notification-setting/slack-configuration', {
+    const response = await apiv3Put('/notification-setting/slack-configuration', {
       webhookUrl: this.state.webhookUrl,
       webhookUrl: this.state.webhookUrl,
       isIncomingWebhookPrioritized: this.state.isIncomingWebhookPrioritized,
       isIncomingWebhookPrioritized: this.state.isIncomingWebhookPrioritized,
       slackToken: this.state.slackToken,
       slackToken: this.state.slackToken,
@@ -71,7 +75,7 @@ export default class AdminNotificationContainer extends Container {
    * @memberOf SlackAppConfiguration
    * @memberOf SlackAppConfiguration
    */
    */
   async addNotificationPattern(pathPattern, channel) {
   async addNotificationPattern(pathPattern, channel) {
-    const response = await this.appContainer.apiv3.post('/notification-setting/user-notification', {
+    const response = await apiv3Post('/notification-setting/user-notification', {
       pathPattern,
       pathPattern,
       channel,
       channel,
     });
     });
@@ -83,7 +87,7 @@ export default class AdminNotificationContainer extends Container {
    * Delete user trigger notification pattern
    * Delete user trigger notification pattern
    */
    */
   async deleteUserTriggerNotificationPattern(notificatiionId) {
   async deleteUserTriggerNotificationPattern(notificatiionId) {
-    const response = await this.appContainer.apiv3.delete(`/notification-setting/user-notification/${notificatiionId}`);
+    const response = await apiv3Delete(`/notification-setting/user-notification/${notificatiionId}`);
     const deletedNotificaton = response.data;
     const deletedNotificaton = response.data;
     await this.retrieveNotificationData();
     await this.retrieveNotificationData();
     return deletedNotificaton;
     return deletedNotificaton;
@@ -108,7 +112,7 @@ export default class AdminNotificationContainer extends Container {
    * @memberOf SlackAppConfiguration
    * @memberOf SlackAppConfiguration
    */
    */
   async updateGlobalNotificationForPages() {
   async updateGlobalNotificationForPages() {
-    const response = await this.appContainer.apiv3.put('/notification-setting/notify-for-page-grant/', {
+    const response = await apiv3Put('/notification-setting/notify-for-page-grant/', {
       isNotificationForOwnerPageEnabled: this.state.isNotificationForOwnerPageEnabled,
       isNotificationForOwnerPageEnabled: this.state.isNotificationForOwnerPageEnabled,
       isNotificationForGroupPageEnabled: this.state.isNotificationForGroupPageEnabled,
       isNotificationForGroupPageEnabled: this.state.isNotificationForGroupPageEnabled,
     });
     });
@@ -120,7 +124,7 @@ export default class AdminNotificationContainer extends Container {
    * Delete global notification pattern
    * Delete global notification pattern
    */
    */
   async deleteGlobalNotificationPattern(notificatiionId) {
   async deleteGlobalNotificationPattern(notificatiionId) {
-    const response = await this.appContainer.apiv3.delete(`/notification-setting/global-notification/${notificatiionId}`);
+    const response = await apiv3Delete(`/notification-setting/global-notification/${notificatiionId}`);
     const deletedNotificaton = response.data;
     const deletedNotificaton = response.data;
     await this.retrieveNotificationData();
     await this.retrieveNotificationData();
     return deletedNotificaton;
     return deletedNotificaton;

+ 6 - 4
packages/app/src/client/services/AdminOidcSecurityContainer.js

@@ -1,10 +1,12 @@
-import { Container } from 'unstated';
-
 import { pathUtils } from '@growi/core';
 import { pathUtils } from '@growi/core';
+import { Container } from 'unstated';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
+
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';
 
 
+import { apiv3Get, apiv3Put } from '../util/apiv3-client';
+
 const logger = loggerFactory('growi:services:AdminLdapSecurityContainer');
 const logger = loggerFactory('growi:services:AdminLdapSecurityContainer');
 
 
 /**
 /**
@@ -51,7 +53,7 @@ export default class AdminOidcSecurityContainer extends Container {
    */
    */
   async retrieveSecurityData() {
   async retrieveSecurityData() {
     try {
     try {
-      const response = await this.appContainer.apiv3.get('/security-setting/');
+      const response = await apiv3Get('/security-setting/');
       const { oidcAuth } = response.data.securityParams;
       const { oidcAuth } = response.data.securityParams;
       this.setState({
       this.setState({
         oidcProviderName: oidcAuth.oidcProviderName,
         oidcProviderName: oidcAuth.oidcProviderName,
@@ -261,7 +263,7 @@ export default class AdminOidcSecurityContainer extends Container {
     };
     };
 
 
     requestParams = await removeNullPropertyFromObject(requestParams);
     requestParams = await removeNullPropertyFromObject(requestParams);
-    const response = await this.appContainer.apiv3.put('/security-setting/oidc', requestParams);
+    const response = await apiv3Put('/security-setting/oidc', requestParams);
     const { securitySettingParams } = response.data;
     const { securitySettingParams } = response.data;
 
 
     this.setState({
     this.setState({

+ 6 - 5
packages/app/src/client/services/AdminSamlSecurityContainer.js

@@ -1,11 +1,12 @@
-import { Container } from 'unstated';
-
-
 import { pathUtils } from '@growi/core';
 import { pathUtils } from '@growi/core';
+import { Container } from 'unstated';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
+
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';
 
 
+import { apiv3Get, apiv3Put } from '../util/apiv3-client';
+
 const logger = loggerFactory('growi:security:AdminSamlSecurityContainer');
 const logger = loggerFactory('growi:security:AdminSamlSecurityContainer');
 
 
 /**
 /**
@@ -57,7 +58,7 @@ export default class AdminSamlSecurityContainer extends Container {
    */
    */
   async retrieveSecurityData() {
   async retrieveSecurityData() {
     try {
     try {
-      const response = await this.appContainer.apiv3.get('/security-setting/');
+      const response = await apiv3Get('/security-setting/');
       const { samlAuth } = response.data.securityParams;
       const { samlAuth } = response.data.securityParams;
       this.setState({
       this.setState({
         missingMandatoryConfigKeys: samlAuth.missingMandatoryConfigKeys,
         missingMandatoryConfigKeys: samlAuth.missingMandatoryConfigKeys,
@@ -195,7 +196,7 @@ export default class AdminSamlSecurityContainer extends Container {
     };
     };
 
 
     requestParams = await removeNullPropertyFromObject(requestParams);
     requestParams = await removeNullPropertyFromObject(requestParams);
-    const response = await this.appContainer.apiv3.put('/security-setting/saml', requestParams);
+    const response = await apiv3Put('/security-setting/saml', requestParams);
     const { securitySettingParams } = response.data;
     const { securitySettingParams } = response.data;
 
 
     this.setState({
     this.setState({

+ 4 - 2
packages/app/src/client/services/AdminSlackIntegrationLegacyContainer.js

@@ -1,5 +1,7 @@
 import { Container } from 'unstated';
 import { Container } from 'unstated';
 
 
+import { apiv3Get, apiv3Put } from '../util/apiv3-client';
+
 /**
 /**
  * Service container for admin LegacySlackIntegration setting page (LegacySlackIntegration.jsx)
  * Service container for admin LegacySlackIntegration setting page (LegacySlackIntegration.jsx)
  * @extends {Container} unstated Container
  * @extends {Container} unstated Container
@@ -35,7 +37,7 @@ export default class AdminSlackIntegrationLegacyContainer extends Container {
    * Retrieve notificationData
    * Retrieve notificationData
    */
    */
   async retrieveData() {
   async retrieveData() {
-    const response = await this.appContainer.apiv3.get('/slack-integration-legacy-settings/');
+    const response = await apiv3Get('/slack-integration-legacy-settings/');
     const { slackIntegrationParams } = response.data;
     const { slackIntegrationParams } = response.data;
 
 
     this.setState({
     this.setState({
@@ -79,7 +81,7 @@ export default class AdminSlackIntegrationLegacyContainer extends Container {
    * @memberOf SlackAppConfiguration
    * @memberOf SlackAppConfiguration
    */
    */
   async updateSlackAppConfiguration() {
   async updateSlackAppConfiguration() {
-    const response = await this.appContainer.apiv3.put('/slack-integration-legacy-settings/', {
+    const response = await apiv3Put('/slack-integration-legacy-settings/', {
       webhookUrl: this.state.webhookUrl,
       webhookUrl: this.state.webhookUrl,
       isIncomingWebhookPrioritized: this.state.isIncomingWebhookPrioritized,
       isIncomingWebhookPrioritized: this.state.isIncomingWebhookPrioritized,
       slackToken: this.state.slackToken,
       slackToken: this.state.slackToken,

+ 6 - 4
packages/app/src/client/services/AdminTwitterSecurityContainer.js

@@ -1,10 +1,12 @@
-import { Container } from 'unstated';
-
 import { pathUtils } from '@growi/core';
 import { pathUtils } from '@growi/core';
+import { Container } from 'unstated';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
+
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';
 
 
+import { apiv3Get, apiv3Put } from '../util/apiv3-client';
+
 const logger = loggerFactory('growi:security:AdminTwitterSecurityContainer');
 const logger = loggerFactory('growi:security:AdminTwitterSecurityContainer');
 
 
 /**
 /**
@@ -36,7 +38,7 @@ export default class AdminTwitterSecurityContainer extends Container {
    */
    */
   async retrieveSecurityData() {
   async retrieveSecurityData() {
     try {
     try {
-      const response = await this.appContainer.apiv3.get('/security-setting/');
+      const response = await apiv3Get('/security-setting/');
       const { twitterOAuth } = response.data.securityParams;
       const { twitterOAuth } = response.data.securityParams;
       this.setState({
       this.setState({
         twitterConsumerKey: twitterOAuth.twitterConsumerKey,
         twitterConsumerKey: twitterOAuth.twitterConsumerKey,
@@ -88,7 +90,7 @@ export default class AdminTwitterSecurityContainer extends Container {
     let requestParams = { twitterConsumerKey, twitterConsumerSecret, isSameUsernameTreatedAsIdenticalUser };
     let requestParams = { twitterConsumerKey, twitterConsumerSecret, isSameUsernameTreatedAsIdenticalUser };
 
 
     requestParams = await removeNullPropertyFromObject(requestParams);
     requestParams = await removeNullPropertyFromObject(requestParams);
-    const response = await this.appContainer.apiv3.put('/security-setting/twitter-oauth', requestParams);
+    const response = await apiv3Put('/security-setting/twitter-oauth', requestParams);
     const { securitySettingParams } = response.data;
     const { securitySettingParams } = response.data;
 
 
     this.setState({
     this.setState({

+ 13 - 8
packages/app/src/client/services/AdminUsersContainer.js

@@ -1,7 +1,12 @@
-import { Container } from 'unstated';
 import { debounce } from 'throttle-debounce';
 import { debounce } from 'throttle-debounce';
+import { Container } from 'unstated';
+
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
+import {
+  apiv3Delete, apiv3Get, apiv3Post, apiv3Put,
+} from '../util/apiv3-client';
+
 // eslint-disable-next-line no-unused-vars
 // eslint-disable-next-line no-unused-vars
 const logger = loggerFactory('growi:services:AdminUserGroupDetailContainer');
 const logger = loggerFactory('growi:services:AdminUserGroupDetailContainer');
 
 
@@ -135,7 +140,7 @@ export default class AdminUsersContainer extends Container {
       // Even if email is hidden, it will be displayed on admin page.
       // Even if email is hidden, it will be displayed on admin page.
       forceIncludeAttributes: ['email'],
       forceIncludeAttributes: ['email'],
     };
     };
-    const { data } = await this.appContainer.apiv3.get('/users', params);
+    const { data } = await apiv3Get('/users', params);
 
 
     if (data.paginateResult == null) {
     if (data.paginateResult == null) {
       throw new Error('data must conclude \'paginateResult\' property.');
       throw new Error('data must conclude \'paginateResult\' property.');
@@ -159,7 +164,7 @@ export default class AdminUsersContainer extends Container {
    * @param {bool} sendEmail
    * @param {bool} sendEmail
    */
    */
   async createUserInvited(shapedEmailList, sendEmail) {
   async createUserInvited(shapedEmailList, sendEmail) {
-    const response = await this.appContainer.apiv3.post('/users/invite', {
+    const response = await apiv3Post('/users/invite', {
       shapedEmailList,
       shapedEmailList,
       sendEmail,
       sendEmail,
     });
     });
@@ -205,7 +210,7 @@ export default class AdminUsersContainer extends Container {
    * @return {string} username
    * @return {string} username
    */
    */
   async giveUserAdmin(userId) {
   async giveUserAdmin(userId) {
-    const response = await this.appContainer.apiv3.put(`/users/${userId}/giveAdmin`);
+    const response = await apiv3Put(`/users/${userId}/giveAdmin`);
     const { username } = response.data.userData;
     const { username } = response.data.userData;
     await this.retrieveUsersByPagingNum(this.state.activePage);
     await this.retrieveUsersByPagingNum(this.state.activePage);
     return username;
     return username;
@@ -218,7 +223,7 @@ export default class AdminUsersContainer extends Container {
    * @return {string} username
    * @return {string} username
    */
    */
   async removeUserAdmin(userId) {
   async removeUserAdmin(userId) {
-    const response = await this.appContainer.apiv3.put(`/users/${userId}/removeAdmin`);
+    const response = await apiv3Put(`/users/${userId}/removeAdmin`);
     const { username } = response.data.userData;
     const { username } = response.data.userData;
     await this.retrieveUsersByPagingNum(this.state.activePage);
     await this.retrieveUsersByPagingNum(this.state.activePage);
     return username;
     return username;
@@ -231,7 +236,7 @@ export default class AdminUsersContainer extends Container {
    * @return {string} username
    * @return {string} username
    */
    */
   async activateUser(userId) {
   async activateUser(userId) {
-    const response = await this.appContainer.apiv3.put(`/users/${userId}/activate`);
+    const response = await apiv3Put(`/users/${userId}/activate`);
     const { username } = response.data.userData;
     const { username } = response.data.userData;
     await this.retrieveUsersByPagingNum(this.state.activePage);
     await this.retrieveUsersByPagingNum(this.state.activePage);
     return username;
     return username;
@@ -244,7 +249,7 @@ export default class AdminUsersContainer extends Container {
    * @return {string} username
    * @return {string} username
    */
    */
   async deactivateUser(userId) {
   async deactivateUser(userId) {
-    const response = await this.appContainer.apiv3.put(`/users/${userId}/deactivate`);
+    const response = await apiv3Put(`/users/${userId}/deactivate`);
     const { username } = response.data.userData;
     const { username } = response.data.userData;
     await this.retrieveUsersByPagingNum(this.state.activePage);
     await this.retrieveUsersByPagingNum(this.state.activePage);
     return username;
     return username;
@@ -257,7 +262,7 @@ export default class AdminUsersContainer extends Container {
    * @return {object} removedUserData
    * @return {object} removedUserData
    */
    */
   async removeUser(userId) {
   async removeUser(userId) {
-    const response = await this.appContainer.apiv3.delete(`/users/${userId}/remove`);
+    const response = await apiv3Delete(`/users/${userId}/remove`);
     const removedUserData = response.data.userData;
     const removedUserData = response.data.userData;
     await this.retrieveUsersByPagingNum(this.state.activePage);
     await this.retrieveUsersByPagingNum(this.state.activePage);
     return removedUserData;
     return removedUserData;

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

@@ -2,20 +2,7 @@ import { Container } from 'unstated';
 
 
 import InterceptorManager from '~/services/interceptor-manager';
 import InterceptorManager from '~/services/interceptor-manager';
 
 
-import {
-  apiDelete, apiGet, apiPost, apiRequest,
-} from '../util/apiv1-client';
-import {
-  apiv3Delete, apiv3Get, apiv3Post, apiv3Put,
-} from '../util/apiv3-client';
-import emojiStrategy from '../util/emojione/emoji_strategy_shrinked.json';
 import GrowiRenderer from '../util/GrowiRenderer';
 import GrowiRenderer from '../util/GrowiRenderer';
-
-import {
-  mediaQueryListForDarkMode,
-  applyColorScheme,
-} from '../util/color-scheme';
-
 import { i18nFactory } from '../util/i18n';
 import { i18nFactory } from '../util/i18n';
 
 
 /**
 /**
@@ -27,10 +14,6 @@ export default class AppContainer extends Container {
   constructor() {
   constructor() {
     super();
     super();
 
 
-    this.state = {
-      preferDarkModeByMediaQuery: false,
-    };
-
     // get csrf token from body element
     // get csrf token from body element
     // DO NOT REMOVE: uploading attachment data requires appContainer.csrfToken
     // DO NOT REMOVE: uploading attachment data requires appContainer.csrfToken
     const body = document.querySelector('body');
     const body = document.querySelector('body');
@@ -38,9 +21,6 @@ export default class AppContainer extends Container {
 
 
     this.config = JSON.parse(document.getElementById('growi-context-hydrate').textContent || '{}');
     this.config = JSON.parse(document.getElementById('growi-context-hydrate').textContent || '{}');
 
 
-    const userAgent = window.navigator.userAgent.toLowerCase();
-    this.isMobile = /iphone|ipad|android/.test(userAgent);
-
     const currentUserElem = document.getElementById('growi-current-user');
     const currentUserElem = document.getElementById('growi-current-user');
     if (currentUserElem != null) {
     if (currentUserElem != null) {
       this.currentUser = JSON.parse(currentUserElem.textContent);
       this.currentUser = JSON.parse(currentUserElem.textContent);
@@ -58,23 +38,6 @@ export default class AppContainer extends Container {
     this.containerInstances = {};
     this.containerInstances = {};
     this.componentInstances = {};
     this.componentInstances = {};
     this.rendererInstances = {};
     this.rendererInstances = {};
-
-    this.apiGet = apiGet;
-    this.apiPost = apiPost;
-    this.apiDelete = apiDelete;
-    this.apiRequest = apiRequest;
-
-    this.apiv3Get = apiv3Get;
-    this.apiv3Post = apiv3Post;
-    this.apiv3Put = apiv3Put;
-    this.apiv3Delete = apiv3Delete;
-
-    this.apiv3 = {
-      get: apiv3Get,
-      post: apiv3Post,
-      put: apiv3Put,
-      delete: apiv3Delete,
-    };
   }
   }
 
 
   /**
   /**
@@ -85,27 +48,18 @@ export default class AppContainer extends Container {
   }
   }
 
 
   initApp() {
   initApp() {
-    this.initMediaQueryForColorScheme();
-
     this.injectToWindow();
     this.injectToWindow();
   }
   }
 
 
   initContents() {
   initContents() {
     const body = document.querySelector('body');
     const body = document.querySelector('body');
 
 
-    this.isAdmin = body.dataset.isAdmin === 'true';
-
     this.isDocSaved = true;
     this.isDocSaved = true;
 
 
     this.originRenderer = new GrowiRenderer(this);
     this.originRenderer = new GrowiRenderer(this);
 
 
     this.interceptorManager = new InterceptorManager();
     this.interceptorManager = new InterceptorManager();
 
 
-    if (this.currentUser != null) {
-      // remove old user cache
-      this.removeOldUserCache();
-    }
-
     const isPluginEnabled = body.dataset.pluginEnabled === 'true';
     const isPluginEnabled = body.dataset.pluginEnabled === 'true';
     if (isPluginEnabled) {
     if (isPluginEnabled) {
       this.initPlugins();
       this.initPlugins();
@@ -114,18 +68,6 @@ export default class AppContainer extends Container {
     this.injectToWindow();
     this.injectToWindow();
   }
   }
 
 
-  async initMediaQueryForColorScheme() {
-    const switchStateByMediaQuery = async(mql) => {
-      const preferDarkMode = mql.matches;
-      this.setState({ preferDarkModeByMediaQuery: preferDarkMode });
-
-      applyColorScheme();
-    };
-
-    // add event listener
-    mediaQueryListForDarkMode.addListener(switchStateByMediaQuery);
-  }
-
   initPlugins() {
   initPlugins() {
     const growiPlugin = window.growiPlugin;
     const growiPlugin = window.growiPlugin;
     growiPlugin.installAll(this, this.originRenderer);
     growiPlugin.installAll(this, this.originRenderer);
@@ -222,28 +164,6 @@ export default class AppContainer extends Container {
     return this.componentInstances[id];
     return this.componentInstances[id];
   }
   }
 
 
-  /**
-   *
-   * @param {string} breakpoint id of breakpoint
-   * @param {function} handler event handler for media query
-   * @param {boolean} invokeOnInit invoke handler after the initialization if true
-   */
-  addBreakpointListener(breakpoint, handler, invokeOnInit = false) {
-    document.addEventListener('DOMContentLoaded', () => {
-      // get the value of '--breakpoint-*'
-      const breakpointPixel = parseInt(window.getComputedStyle(document.documentElement).getPropertyValue(`--breakpoint-${breakpoint}`), 10);
-
-      const mediaQuery = window.matchMedia(`(min-width: ${breakpointPixel}px)`);
-
-      // add event listener
-      mediaQuery.addListener(handler);
-      // initialize
-      if (invokeOnInit) {
-        handler(mediaQuery);
-      }
-    });
-  }
-
   getOriginRenderer() {
   getOriginRenderer() {
     return this.originRenderer;
     return this.originRenderer;
   }
   }
@@ -266,21 +186,6 @@ export default class AppContainer extends Container {
     return renderer;
     return renderer;
   }
   }
 
 
-  getEmojiStrategy() {
-    return emojiStrategy;
-  }
-
-  removeOldUserCache() {
-    if (window.localStorage.userByName == null) {
-      return;
-    }
-
-    const keys = ['userByName', 'userById', 'users', 'lastFetched'];
-
-    keys.forEach((key) => {
-      window.localStorage.removeItem(key);
-    });
-  }
 
 
   launchHandsontableModal(componentKind, beginLineNumber, endLineNumber) {
   launchHandsontableModal(componentKind, beginLineNumber, endLineNumber) {
     let targetComponent;
     let targetComponent;

+ 9 - 6
packages/app/src/client/services/CommentContainer.js

@@ -2,6 +2,9 @@ import { Container } from 'unstated';
 
 
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
+import { apiGet, apiPost } from '../util/apiv1-client';
+import { apiv3Put } from '../util/apiv3-client';
+
 const logger = loggerFactory('growi:services:CommentContainer');
 const logger = loggerFactory('growi:services:CommentContainer');
 
 
 /**
 /**
@@ -67,7 +70,7 @@ export default class CommentContainer extends Container {
     const { pageId } = this.getPageContainer().state;
     const { pageId } = this.getPageContainer().state;
 
 
     // get data (desc order array)
     // get data (desc order array)
-    const res = await this.appContainer.apiGet('/comments.get', { page_id: pageId });
+    const res = await apiGet('/comments.get', { page_id: pageId });
     if (res.ok) {
     if (res.ok) {
       const comments = res.comments;
       const comments = res.comments;
       this.setState({ comments });
       this.setState({ comments });
@@ -89,7 +92,7 @@ export default class CommentContainer extends Container {
     }
     }
 
 
     try {
     try {
-      await this.appContainer.apiv3Put('/users/update.imageUrlCache', { userIds: noImageCacheUserIds });
+      await apiv3Put('/users/update.imageUrlCache', { userIds: noImageCacheUserIds });
     }
     }
     catch (err) {
     catch (err) {
       // Error alert doesn't apear, because user don't need to notice this error.
       // Error alert doesn't apear, because user don't need to notice this error.
@@ -103,7 +106,7 @@ export default class CommentContainer extends Container {
   postComment(comment, isMarkdown, replyTo, isSlackEnabled, slackChannels) {
   postComment(comment, isMarkdown, replyTo, isSlackEnabled, slackChannels) {
     const { pageId, revisionId } = this.getPageContainer().state;
     const { pageId, revisionId } = this.getPageContainer().state;
 
 
-    return this.appContainer.apiPost('/comments.add', {
+    return apiPost('/comments.add', {
       commentForm: {
       commentForm: {
         comment,
         comment,
         page_id: pageId,
         page_id: pageId,
@@ -129,7 +132,7 @@ export default class CommentContainer extends Container {
   putComment(comment, isMarkdown, commentId, author) {
   putComment(comment, isMarkdown, commentId, author) {
     const { pageId, revisionId } = this.getPageContainer().state;
     const { pageId, revisionId } = this.getPageContainer().state;
 
 
-    return this.appContainer.apiPost('/comments.update', {
+    return apiPost('/comments.update', {
       commentForm: {
       commentForm: {
         comment,
         comment,
         is_markdown: isMarkdown,
         is_markdown: isMarkdown,
@@ -145,7 +148,7 @@ export default class CommentContainer extends Container {
   }
   }
 
 
   deleteComment(comment) {
   deleteComment(comment) {
-    return this.appContainer.apiPost('/comments.remove', { comment_id: comment._id })
+    return apiPost('/comments.remove', { comment_id: comment._id })
       .then((res) => {
       .then((res) => {
         if (res.ok) {
         if (res.ok) {
           this.findAndSplice(comment);
           this.findAndSplice(comment);
@@ -163,7 +166,7 @@ export default class CommentContainer extends Container {
     formData.append('path', pagePath);
     formData.append('path', pagePath);
     formData.append('page_id', pageId);
     formData.append('page_id', pageId);
 
 
-    return this.appContainer.apiPost(endpoint, formData);
+    return apiPost(endpoint, formData);
   }
   }
 
 
 }
 }

+ 3 - 0
packages/app/src/client/services/ContextExtractor.tsx

@@ -18,6 +18,7 @@ import {
   useShareLinkId, useShareLinksNumber, useTemplateTagData, useCurrentUpdatedAt, useCreator, useRevisionAuthor, useCurrentUser, useTargetAndAncestors,
   useShareLinkId, useShareLinksNumber, useTemplateTagData, useCurrentUpdatedAt, useCreator, useRevisionAuthor, useCurrentUser, useTargetAndAncestors,
   useSlackChannels, useNotFoundTargetPathOrId, useIsSearchPage, useIsForbidden, useIsIdenticalPath,
   useSlackChannels, useNotFoundTargetPathOrId, useIsSearchPage, useIsForbidden, useIsIdenticalPath,
   useIsAclEnabled, useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsEnabledAttachTitleHeader, useIsNotFoundPermalink,
   useIsAclEnabled, useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsEnabledAttachTitleHeader, useIsNotFoundPermalink,
+  useDefaultIndentSize, useIsIndentSizeForced,
 } from '../../stores/context';
 } from '../../stores/context';
 
 
 const { isTrashPage: _isTrashPage } = pagePathUtils;
 const { isTrashPage: _isTrashPage } = pagePathUtils;
@@ -109,6 +110,8 @@ const ContextExtractorOnce: FC = () => {
   useIsSearchServiceConfigured(configByContextHydrate.isSearchServiceConfigured);
   useIsSearchServiceConfigured(configByContextHydrate.isSearchServiceConfigured);
   useIsSearchServiceReachable(configByContextHydrate.isSearchServiceReachable);
   useIsSearchServiceReachable(configByContextHydrate.isSearchServiceReachable);
   useIsEnabledAttachTitleHeader(configByContextHydrate.isEnabledAttachTitleHeader);
   useIsEnabledAttachTitleHeader(configByContextHydrate.isEnabledAttachTitleHeader);
+  useIsIndentSizeForced(configByContextHydrate.isIndentSizeForced);
+  useDefaultIndentSize(configByContextHydrate.adminPreferredIndentSize);
 
 
 
 
   // Page
   // Page

+ 2 - 77
packages/app/src/client/services/EditorContainer.js

@@ -4,46 +4,27 @@ import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:services:EditorContainer');
 const logger = loggerFactory('growi:services:EditorContainer');
 
 
+
 /**
 /**
  * Service container related to options for Editor/Preview
  * Service container related to options for Editor/Preview
  * @extends {Container} unstated Container
  * @extends {Container} unstated Container
  */
  */
 export default class EditorContainer extends Container {
 export default class EditorContainer extends Container {
 
 
-  constructor(appContainer, defaultEditorOptions, defaultPreviewOptions) {
+  constructor(appContainer) {
     super();
     super();
 
 
     this.appContainer = appContainer;
     this.appContainer = appContainer;
     this.appContainer.registerContainer(this);
     this.appContainer.registerContainer(this);
-    this.retrieveEditorSettings = this.retrieveEditorSettings.bind(this);
-
-    const mainContent = document.querySelector('#content-main');
-
-    if (mainContent == null) {
-      logger.debug('#content-main element is not exists');
-      return;
-    }
 
 
     this.state = {
     this.state = {
       tags: null,
       tags: null,
-
-      editorOptions: {},
-      previewOptions: {},
-
-      // Defaults to null to show modal when not in DB
-      isTextlintEnabled: null,
-      textlintRules: [],
-
-      indentSize: this.appContainer.config.adminPreferredIndentSize || 4,
     };
     };
 
 
     this.isSetBeforeunloadEventHandler = false;
     this.isSetBeforeunloadEventHandler = false;
 
 
     this.initDrafts();
     this.initDrafts();
 
 
-    this.editorOptions = null;
-    this.initEditorOptions('editorOptions', 'editorOptions', defaultEditorOptions);
-    this.initEditorOptions('previewOptions', 'previewOptions', defaultPreviewOptions);
   }
   }
 
 
   /**
   /**
@@ -78,30 +59,6 @@ export default class EditorContainer extends Container {
     }
     }
   }
   }
 
 
-  initEditorOptions(stateKey, localStorageKey, defaultOptions) {
-    // load from localStorage
-    const optsStr = window.localStorage[localStorageKey];
-
-    let loadedOpts = {};
-    // JSON.parseparse
-    if (optsStr != null) {
-      try {
-        loadedOpts = JSON.parse(optsStr);
-      }
-      catch (e) {
-        this.localStorage.removeItem(localStorageKey);
-      }
-    }
-
-    // set to state obj
-    this.state[stateKey] = Object.assign(defaultOptions, loadedOpts);
-  }
-
-  saveOptsToLocalStorage() {
-    window.localStorage.setItem('editorOptions', JSON.stringify(this.state.editorOptions));
-    window.localStorage.setItem('previewOptions', JSON.stringify(this.state.previewOptions));
-  }
-
   setCaretLine(line) {
   setCaretLine(line) {
     const pageEditor = this.appContainer.getComponentInstance('PageEditor');
     const pageEditor = this.appContainer.getComponentInstance('PageEditor');
     if (pageEditor != null) {
     if (pageEditor != null) {
@@ -116,19 +73,11 @@ export default class EditorContainer extends Container {
     }
     }
   }
   }
 
 
-  // TODO: Remove when SWR is complete
   getCurrentOptionsToSave() {
   getCurrentOptionsToSave() {
     const opt = {
     const opt = {
-      // isSlackEnabled: this.state.isSlackEnabled,
-      // slackChannels: this.state.slackChannels,
-      // grant: this.state.grant,
       pageTags: this.state.tags,
       pageTags: this.state.tags,
     };
     };
 
 
-    // if (this.state.grantGroupId != null) {
-    //   opt.grantUserGroupId = this.state.grantGroupId;
-    // }
-
     return opt;
     return opt;
   }
   }
 
 
@@ -175,28 +124,4 @@ export default class EditorContainer extends Container {
     return null;
     return null;
   }
   }
 
 
-
-  /**
-   * Retrieve Editor Settings
-   */
-  async retrieveEditorSettings() {
-    if (this.appContainer.isGuestUser) {
-      return;
-    }
-
-    const { data } = await this.appContainer.apiv3Get('/personal-setting/editor-settings');
-
-    if (data?.textlintSettings == null) {
-      return;
-    }
-
-    // Defaults to null to show modal when not in DB
-    const { isTextlintEnabled = null, textlintRules = [] } = data.textlintSettings;
-
-    this.setState({
-      isTextlintEnabled,
-      textlintRules,
-    });
-  }
-
 }
 }

+ 32 - 11
packages/app/src/client/services/PageContainer.js

@@ -1,22 +1,23 @@
-import { Container } from 'unstated';
-
-
+import { pagePathUtils } from '@growi/core';
 import * as entities from 'entities';
 import * as entities from 'entities';
 import * as toastr from 'toastr';
 import * as toastr from 'toastr';
-import { pagePathUtils } from '@growi/core';
+import { Container } from 'unstated';
+
 
 
-import loggerFactory from '~/utils/logger';
 import { EditorMode } from '~/stores/ui';
 import { EditorMode } from '~/stores/ui';
+import loggerFactory from '~/utils/logger';
 
 
 import { toastError } from '../util/apiNotification';
 import { toastError } from '../util/apiNotification';
+import { apiPost } from '../util/apiv1-client';
+import { apiv3Post } from '../util/apiv3-client';
 import {
 import {
   DetachCodeBlockInterceptor,
   DetachCodeBlockInterceptor,
   RestoreCodeBlockInterceptor,
   RestoreCodeBlockInterceptor,
 } from '../util/interceptor/detach-code-blocks';
 } from '../util/interceptor/detach-code-blocks';
-
 import {
 import {
   DrawioInterceptor,
   DrawioInterceptor,
 } from '../util/interceptor/drawio-interceptor';
 } from '../util/interceptor/drawio-interceptor';
+import { emojiMartData } from '../util/markdown-it/emoji-mart-data';
 
 
 const { isTrashPage } = pagePathUtils;
 const { isTrashPage } = pagePathUtils;
 
 
@@ -120,7 +121,7 @@ export default class PageContainer extends Container {
     if (unlinkPageButton != null) {
     if (unlinkPageButton != null) {
       unlinkPageButton.addEventListener('click', async() => {
       unlinkPageButton.addEventListener('click', async() => {
         try {
         try {
-          const res = await this.appContainer.apiPost('/pages.unlink', { path });
+          const res = await apiPost('/pages.unlink', { path });
           window.location.href = encodeURI(`${res.path}?unlinked=true`);
           window.location.href = encodeURI(`${res.path}?unlinked=true`);
         }
         }
         catch (err) {
         catch (err) {
@@ -194,12 +195,32 @@ export default class PageContainer extends Container {
     this.setState(newState);
     this.setState(newState);
   }
   }
 
 
-  setTocHtml(tocHtml) {
+  async setTocHtml(tocHtml) {
     if (this.state.tocHtml !== tocHtml) {
     if (this.state.tocHtml !== tocHtml) {
-      this.setState({ tocHtml });
+      const tocHtmlWithEmoji = await this.colonsToEmoji(tocHtml);
+      this.setState({ tocHtml: tocHtmlWithEmoji });
     }
     }
   }
   }
 
 
+  /**
+   *
+   * @param {*} html TOC html string
+   * @returns TOC html with emoji (emoji-mart) in URL
+   */
+  async colonsToEmoji(html) {
+    // Emoji colons matching
+    const colons = ':[a-zA-Z0-9-_+]+:';
+    // Emoji with skin tone matching
+    const skin = ':skin-tone-[2-6]:';
+    const colonsRegex = new RegExp(`(${colons}${skin}|${colons})`, 'g');
+    const emojiData = await emojiMartData();
+    return html.replace(colonsRegex, (index, match) => {
+      const emojiName = match.slice(1, -1);
+      return emojiData[emojiName];
+    });
+
+  }
+
   /**
   /**
    * save success handler
    * save success handler
    * @param {object} page Page instance
    * @param {object} page Page instance
@@ -350,7 +371,7 @@ export default class PageContainer extends Container {
       body: markdown,
       body: markdown,
     });
     });
 
 
-    const res = await this.appContainer.apiv3Post('/pages/', params);
+    const res = await apiv3Post('/pages/', params);
     const { page, tags, revision } = res.data;
     const { page, tags, revision } = res.data;
 
 
     return { page, tags, revision };
     return { page, tags, revision };
@@ -366,7 +387,7 @@ export default class PageContainer extends Container {
       body: markdown,
       body: markdown,
     });
     });
 
 
-    const res = await this.appContainer.apiPost('/pages.update', params);
+    const res = await apiPost('/pages.update', params);
     if (!res.ok) {
     if (!res.ok) {
       throw new Error(res.error);
       throw new Error(res.error);
     }
     }

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

@@ -3,6 +3,7 @@ import { Container } from 'unstated';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { toastError } from '../util/apiNotification';
 import { toastError } from '../util/apiNotification';
+import { apiv3Get } from '../util/apiv3-client';
 
 
 const logger = loggerFactory('growi:PageHistoryContainer');
 const logger = loggerFactory('growi:PageHistoryContainer');
 
 
@@ -60,7 +61,7 @@ export default class PageHistoryContainer extends Container {
     }
     }
 
 
     // Get one more for the bottom display
     // Get one more for the bottom display
-    const res = await this.appContainer.apiv3Get('/revisions/list', {
+    const res = await apiv3Get('/revisions/list', {
       pageId, shareLinkId, page, limit: pagingLimitForApiParam,
       pageId, shareLinkId, page, limit: pagingLimitForApiParam,
     });
     });
     const rev = res.data.docs;
     const rev = res.data.docs;
@@ -147,7 +148,7 @@ export default class PageHistoryContainer extends Container {
     }
     }
 
 
     try {
     try {
-      const res = await this.appContainer.apiv3Get(`/revisions/${revision._id}`, { pageId, shareLinkId });
+      const res = await apiv3Get(`/revisions/${revision._id}`, { pageId, shareLinkId });
       this.setState({
       this.setState({
         revisions: this.state.revisions.map((rev) => {
         revisions: this.state.revisions.map((rev) => {
           // comparing ObjectId
           // comparing ObjectId

+ 11 - 8
packages/app/src/client/services/PersonalContainer.js

@@ -2,6 +2,9 @@ import { Container } from 'unstated';
 
 
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
+import { apiPost } from '../util/apiv1-client';
+import { apiv3Get, apiv3Put } from '../util/apiv3-client';
+
 // eslint-disable-next-line no-unused-vars
 // eslint-disable-next-line no-unused-vars
 const logger = loggerFactory('growi:services:PersonalContainer');
 const logger = loggerFactory('growi:services:PersonalContainer');
 
 
@@ -47,7 +50,7 @@ export default class PersonalContainer extends Container {
    */
    */
   async retrievePersonalData() {
   async retrievePersonalData() {
     try {
     try {
-      const response = await this.appContainer.apiv3.get('/personal-setting/');
+      const response = await apiv3Get('/personal-setting/');
       const { currentUser } = response.data;
       const { currentUser } = response.data;
       this.setState({
       this.setState({
         name: currentUser.name,
         name: currentUser.name,
@@ -90,7 +93,7 @@ export default class PersonalContainer extends Container {
    */
    */
   async retrieveExternalAccounts() {
   async retrieveExternalAccounts() {
     try {
     try {
-      const response = await this.appContainer.apiv3.get('/personal-setting/external-accounts');
+      const response = await apiv3Get('/personal-setting/external-accounts');
       const { externalAccounts } = response.data;
       const { externalAccounts } = response.data;
 
 
       this.setState({ externalAccounts });
       this.setState({ externalAccounts });
@@ -151,7 +154,7 @@ export default class PersonalContainer extends Container {
    */
    */
   async updateBasicInfo() {
   async updateBasicInfo() {
     try {
     try {
-      const response = await this.appContainer.apiv3.put('/personal-setting/', {
+      const response = await apiv3Put('/personal-setting/', {
         name: this.state.name,
         name: this.state.name,
         email: this.state.email,
         email: this.state.email,
         isEmailPublished: this.state.isEmailPublished,
         isEmailPublished: this.state.isEmailPublished,
@@ -181,7 +184,7 @@ export default class PersonalContainer extends Container {
    */
    */
   async updateProfileImage() {
   async updateProfileImage() {
     try {
     try {
-      const response = await this.appContainer.apiv3.put('/personal-setting/image-type', {
+      const response = await apiv3Put('/personal-setting/image-type', {
         isGravatarEnabled: this.state.isGravatarEnabled,
         isGravatarEnabled: this.state.isGravatarEnabled,
       });
       });
       const { userData } = response.data;
       const { userData } = response.data;
@@ -204,7 +207,7 @@ export default class PersonalContainer extends Container {
       const formData = new FormData();
       const formData = new FormData();
       formData.append('file', file);
       formData.append('file', file);
       formData.append('_csrf', this.appContainer.csrfToken);
       formData.append('_csrf', this.appContainer.csrfToken);
-      const response = await this.appContainer.apiPost('/attachments.uploadProfileImage', formData);
+      const response = await apiPost('/attachments.uploadProfileImage', formData);
       this.setState({ isUploadedPicture: true, uploadedPictureSrc: response.attachment.filePathProxied });
       this.setState({ isUploadedPicture: true, uploadedPictureSrc: response.attachment.filePathProxied });
     }
     }
     catch (err) {
     catch (err) {
@@ -219,7 +222,7 @@ export default class PersonalContainer extends Container {
    */
    */
   async deleteProfileImage() {
   async deleteProfileImage() {
     try {
     try {
-      await this.appContainer.apiPost('/attachments.removeProfileImage', { _csrf: this.appContainer.csrfToken });
+      await apiPost('/attachments.removeProfileImage', { _csrf: this.appContainer.csrfToken });
       this.setState({ isUploadedPicture: false, uploadedPictureSrc: DEFAULT_IMAGE });
       this.setState({ isUploadedPicture: false, uploadedPictureSrc: DEFAULT_IMAGE });
     }
     }
     catch (err) {
     catch (err) {
@@ -234,7 +237,7 @@ export default class PersonalContainer extends Container {
    */
    */
   async associateLdapAccount(account) {
   async associateLdapAccount(account) {
     try {
     try {
-      await this.appContainer.apiv3.put('/personal-setting/associate-ldap', account);
+      await apiv3Put('/personal-setting/associate-ldap', account);
     }
     }
     catch (err) {
     catch (err) {
       this.setState({ retrieveError: err });
       this.setState({ retrieveError: err });
@@ -248,7 +251,7 @@ export default class PersonalContainer extends Container {
    */
    */
   async disassociateLdapAccount(account) {
   async disassociateLdapAccount(account) {
     try {
     try {
-      await this.appContainer.apiv3.put('/personal-setting/disassociate-ldap', account);
+      await apiv3Put('/personal-setting/disassociate-ldap', account);
     }
     }
     catch (err) {
     catch (err) {
       this.setState({ retrieveError: err });
       this.setState({ retrieveError: err });

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

@@ -3,6 +3,7 @@ import { Container } from 'unstated';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { toastError } from '../util/apiNotification';
 import { toastError } from '../util/apiNotification';
+import { apiv3Get } from '../util/apiv3-client';
 
 
 const logger = loggerFactory('growi:PageHistoryContainer');
 const logger = loggerFactory('growi:PageHistoryContainer');
 
 
@@ -75,7 +76,7 @@ export default class RevisionComparerContainer extends Container {
     const { pageId, shareLinkId } = this.pageContainer.state;
     const { pageId, shareLinkId } = this.pageContainer.state;
 
 
     try {
     try {
-      const res = await this.appContainer.apiv3Get('/revisions/list', {
+      const res = await apiv3Get('/revisions/list', {
         pageId, shareLinkId, page: 1, limit: 1,
         pageId, shareLinkId, page: 1, limit: 1,
       });
       });
       return res.data.docs[0];
       return res.data.docs[0];
@@ -96,7 +97,7 @@ export default class RevisionComparerContainer extends Container {
     const { pageId, shareLinkId } = this.pageContainer.state;
     const { pageId, shareLinkId } = this.pageContainer.state;
 
 
     try {
     try {
-      const res = await this.appContainer.apiv3Get(`/revisions/${revisionId}`, {
+      const res = await apiv3Get(`/revisions/${revisionId}`, {
         pageId, shareLinkId,
         pageId, shareLinkId,
       });
       });
       return res.data.revision;
       return res.data.revision;

+ 3 - 1
packages/app/src/client/services/TagContainer.js

@@ -2,6 +2,8 @@ import { Container } from 'unstated';
 
 
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
+import { apiGet } from '../util/apiv1-client';
+
 const logger = loggerFactory('growi:services:TagContainer');
 const logger = loggerFactory('growi:services:TagContainer');
 
 
 /**
 /**
@@ -48,7 +50,7 @@ export default class TagContainer extends Container {
     let tags = [];
     let tags = [];
     // when the page exists or shared page
     // when the page exists or shared page
     if (pageId != null && shareLinkId == null) {
     if (pageId != null && shareLinkId == null) {
-      const res = await this.appContainer.apiGet('/pages.getPageTag', { pageId });
+      const res = await apiGet('/pages.getPageTag', { pageId });
       tags = res.tags;
       tags = res.tags;
     }
     }
     // when the page not exist
     // when the page not exist

+ 1 - 1
packages/app/src/client/util/editor.ts

@@ -4,7 +4,7 @@ type OptionsToSave = {
   isSlackEnabled: boolean;
   isSlackEnabled: boolean;
   slackChannels: string;
   slackChannels: string;
   grant: number;
   grant: number;
-  pageTags: string[];
+  pageTags: string[] | null;
   grantUserGroupId: string | null;
   grantUserGroupId: string | null;
   grantUserGroupName: string | null;
   grantUserGroupName: string | null;
 };
 };

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
packages/app/src/client/util/emojione/emoji_strategy_shrinked.json


+ 3 - 3
packages/app/src/client/util/interceptor/drawio-interceptor.js

@@ -1,8 +1,9 @@
 /* eslint-disable import/prefer-default-export */
 /* eslint-disable import/prefer-default-export */
 import React from 'react';
 import React from 'react';
+
+import { BasicInterceptor } from '@growi/core';
 import ReactDOM from 'react-dom';
 import ReactDOM from 'react-dom';
 import { Provider } from 'unstated';
 import { Provider } from 'unstated';
-import { BasicInterceptor } from '@growi/core';
 
 
 import Drawio from '~/components/Drawio';
 import Drawio from '~/components/Drawio';
 
 
@@ -103,8 +104,7 @@ export class DrawioInterceptor extends BasicInterceptor {
    */
    */
   drawioPostRender(contextName, context) {
   drawioPostRender(contextName, context) {
     const isPreview = (contextName === 'postRenderPreviewHtml');
     const isPreview = (contextName === 'postRenderPreviewHtml');
-    const editorContainer = this.appContainer.getContainer('EditorContainer');
-    const renderDrawioInRealtime = editorContainer.state.previewOptions.renderDrawioInRealtime;
+    const renderDrawioInRealtime = context.editorSettings?.renderDrawioInRealtime;
 
 
     Object.keys(context.DrawioMap).forEach((domId) => {
     Object.keys(context.DrawioMap).forEach((domId) => {
       const elem = document.getElementById(domId);
       const elem = document.getElementById(domId);

+ 66 - 0
packages/app/src/client/util/markdown-it/emoji-mart-data.ts

@@ -0,0 +1,66 @@
+import { Emoji } from 'emoji-mart';
+import data from 'emoji-mart/data/apple.json';
+
+const DEFAULT_EMOJI_SIZE = 24;
+
+/**
+ *
+ * Get native emoji with skin tone
+ * @param emoji Emoji object
+ * @param skin number
+ * @returns emoji data with skin tone
+ */
+const getEmojiSkinTone = async(emoji) => {
+  const emojiData = {};
+  [...Array(6).keys()].forEach((index) => {
+    if (index > 0) {
+      const elem = Emoji({
+        emoji,
+        skin: index + 1,
+        size: DEFAULT_EMOJI_SIZE,
+      });
+      if (elem) {
+        emojiData[`${emoji}::skin-tone-${index + 1}`] = elem.props['aria-label'].split(',')[0];
+      }
+    }
+  });
+  return emojiData;
+};
+
+/**
+ * Get native emoji from emoji array
+ * @param emojis array of emoji
+ * @returns emoji data
+ */
+
+const getNativeEmoji = async(emojis) => {
+  const emojiData = {};
+  emojis.forEach(async(emoji) => {
+    const emojiName = emoji[0];
+    const hasSkinVariation = emoji[1].skin_variations;
+    const elem = Emoji({
+      emoji: emojiName,
+      size: DEFAULT_EMOJI_SIZE,
+    });
+    if (elem != null) {
+      emojiData[emojiName] = elem.props['aria-label'].split(',')[0];
+      if (hasSkinVariation) {
+        const emojiWithSkinTone = await getEmojiSkinTone(emojiName);
+        Object.assign(emojiData, emojiWithSkinTone);
+      }
+    }
+  });
+  return emojiData;
+};
+
+/**
+ * Get native emoji mart data
+ * @returns native emoji mart data
+ */
+
+export const emojiMartData = () => {
+  const emojis = Object.entries(data.emojis).map((emoji) => {
+    return emoji;
+  });
+  return getNativeEmoji(emojis);
+};

+ 5 - 19
packages/app/src/client/util/markdown-it/emoji.js

@@ -1,3 +1,5 @@
+import { emojiMartData } from './emoji-mart-data';
+
 export default class EmojiConfigurer {
 export default class EmojiConfigurer {
 
 
   constructor(crowi) {
   constructor(crowi) {
@@ -5,25 +7,9 @@ export default class EmojiConfigurer {
   }
   }
 
 
   configure(md) {
   configure(md) {
-    const emojiStrategy = this.crowi.getEmojiStrategy();
-
-    const emojiShortnameUnicodeMap = {};
-
-    /* eslint-disable guard-for-in, no-restricted-syntax */
-    for (const unicode in emojiStrategy) {
-      const data = emojiStrategy[unicode];
-      const shortname = data.shortname.replace(/:/g, '');
-      emojiShortnameUnicodeMap[shortname] = String.fromCharCode(unicode);
-    }
-    /* eslint-enable guard-for-in, no-restricted-syntax */
-
-    md.use(require('markdown-it-emoji'), { defs: emojiShortnameUnicodeMap });
-
-    // integrate markdown-it-emoji and emojione
-    md.renderer.rules.emoji = (token, idx) => {
-      const shortname = `:${token[idx].markup}:`;
-      return emojione.shortnameToImage(shortname);
-    };
+    emojiMartData().then((data) => {
+      md.use(require('markdown-it-emoji-mart'), { defs: data });
+    });
   }
   }
 
 
 }
 }

+ 5 - 4
packages/app/src/components/Admin/Customize/CustomizeLayoutSetting.jsx

@@ -1,10 +1,11 @@
 import React, { useCallback, useEffect, useState } from 'react';
 import React, { useCallback, useEffect, useState } from 'react';
+
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
-
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
 import { isDarkMode as isDarkModeByUtil } from '~/client/util/color-scheme';
 import { isDarkMode as isDarkModeByUtil } from '~/client/util/color-scheme';
 
 
 const isDarkMode = isDarkModeByUtil();
 const isDarkMode = isDarkModeByUtil();
@@ -18,14 +19,14 @@ const CustomizeLayoutSetting = (props) => {
 
 
   const retrieveData = useCallback(async() => {
   const retrieveData = useCallback(async() => {
     try {
     try {
-      const res = await appContainer.apiv3Get('/customize-setting/layout');
+      const res = await apiv3Get('/customize-setting/layout');
       setIsContainerFluid(res.data.isContainerFluid);
       setIsContainerFluid(res.data.isContainerFluid);
     }
     }
     catch (err) {
     catch (err) {
       setRetrieveError(err);
       setRetrieveError(err);
       toastError(err);
       toastError(err);
     }
     }
-  }, [appContainer]);
+  }, []);
 
 
   useEffect(() => {
   useEffect(() => {
     retrieveData();
     retrieveData();
@@ -33,7 +34,7 @@ const CustomizeLayoutSetting = (props) => {
 
 
   const onClickSubmit = async() => {
   const onClickSubmit = async() => {
     try {
     try {
-      await appContainer.apiv3Put('/customize-setting/layout', { isContainerFluid });
+      await apiv3Put('/customize-setting/layout', { isContainerFluid });
       toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.layout') }));
       toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.layout') }));
       retrieveData();
       retrieveData();
     }
     }

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

@@ -1,16 +1,19 @@
 import React from 'react';
 import React from 'react';
+
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
-import AppContainer from '~/client/services/AppContainer';
 import AdminSocketIoContainer from '~/client/services/AdminSocketIoContainer';
 import AdminSocketIoContainer from '~/client/services/AdminSocketIoContainer';
+import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { apiv3Get, apiv3Post, apiv3Put } from '~/client/util/apiv3-client';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
 
 
-import StatusTable from './StatusTable';
-import ReconnectControls from './ReconnectControls';
 import NormalizeIndicesControls from './NormalizeIndicesControls';
 import NormalizeIndicesControls from './NormalizeIndicesControls';
 import RebuildIndexControls from './RebuildIndexControls';
 import RebuildIndexControls from './RebuildIndexControls';
+import ReconnectControls from './ReconnectControls';
+import StatusTable from './StatusTable';
 
 
 class ElasticsearchManagement extends React.Component {
 class ElasticsearchManagement extends React.Component {
 
 
@@ -70,7 +73,7 @@ class ElasticsearchManagement extends React.Component {
     const { appContainer } = this.props;
     const { appContainer } = this.props;
 
 
     try {
     try {
-      const { data } = await appContainer.apiv3Get('/search/indices');
+      const { data } = await apiv3Get('/search/indices');
       const { info } = data;
       const { info } = data;
 
 
       this.setState({
       this.setState({
@@ -105,7 +108,7 @@ class ElasticsearchManagement extends React.Component {
     this.setState({ isReconnectingProcessing: true });
     this.setState({ isReconnectingProcessing: true });
 
 
     try {
     try {
-      await appContainer.apiv3Post('/search/connection');
+      await apiv3Post('/search/connection');
     }
     }
     catch (e) {
     catch (e) {
       toastError(e);
       toastError(e);
@@ -120,7 +123,7 @@ class ElasticsearchManagement extends React.Component {
     const { appContainer } = this.props;
     const { appContainer } = this.props;
 
 
     try {
     try {
-      await appContainer.apiv3Put('/search/indices', { operation: 'normalize' });
+      await apiv3Put('/search/indices', { operation: 'normalize' });
     }
     }
     catch (e) {
     catch (e) {
       toastError(e);
       toastError(e);
@@ -137,7 +140,7 @@ class ElasticsearchManagement extends React.Component {
     this.setState({ isRebuildingProcessing: true });
     this.setState({ isRebuildingProcessing: true });
 
 
     try {
     try {
-      await appContainer.apiv3Put('/search/indices', { operation: 'rebuild' });
+      await apiv3Put('/search/indices', { operation: 'rebuild' });
       toastSuccess('Rebuilding is requested');
       toastSuccess('Rebuilding is requested');
     }
     }
     catch (e) {
     catch (e) {

+ 6 - 3
packages/app/src/components/Admin/ExportArchiveData/SelectCollectionsModal.jsx

@@ -1,4 +1,5 @@
 import React from 'react';
 import React from 'react';
+
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 import {
 import {
@@ -6,8 +7,10 @@ import {
 } from 'reactstrap';
 } from 'reactstrap';
 import * as toastr from 'toastr';
 import * as toastr from 'toastr';
 
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
+import { apiPost } from '~/client/util/apiv1-client';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
 // import { toastSuccess, toastError } from '~/client/util/apiNotification';
 // import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
 
 
 
@@ -67,8 +70,8 @@ class SelectCollectionsModal extends React.Component {
     e.preventDefault();
     e.preventDefault();
 
 
     try {
     try {
-      // TODO: use appContainer.apiv3.post
-      const result = await this.props.appContainer.apiPost('/v3/export', { collections: Array.from(this.state.selectedCollections) });
+      // TODO: use apiv3Post
+      const result = await apiPost('/v3/export', { collections: Array.from(this.state.selectedCollections) });
       // TODO: toastSuccess, toastError
       // TODO: toastSuccess, toastError
 
 
       if (!result.ok) {
       if (!result.ok) {

+ 10 - 7
packages/app/src/components/Admin/ExportArchiveDataPage.jsx

@@ -1,19 +1,22 @@
 import React, { Fragment } from 'react';
 import React, { Fragment } from 'react';
+
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 import * as toastr from 'toastr';
 import * as toastr from 'toastr';
 
 
 
 
+import AdminSocketIoContainer from '~/client/services/AdminSocketIoContainer';
+import AppContainer from '~/client/services/AppContainer';
+import { apiDelete, apiGet } from '~/client/util/apiv1-client';
+
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
 // import { toastSuccess, toastError } from '~/client/util/apiNotification';
 // import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
 
-import AppContainer from '~/client/services/AppContainer';
-import AdminSocketIoContainer from '~/client/services/AdminSocketIoContainer';
 
 
 import LabeledProgressBar from './Common/LabeledProgressBar';
 import LabeledProgressBar from './Common/LabeledProgressBar';
-
-import SelectCollectionsModal from './ExportArchiveData/SelectCollectionsModal';
 import ArchiveFilesTable from './ExportArchiveData/ArchiveFilesTable';
 import ArchiveFilesTable from './ExportArchiveData/ArchiveFilesTable';
+import SelectCollectionsModal from './ExportArchiveData/SelectCollectionsModal';
+
 
 
 const IGNORED_COLLECTION_NAMES = [
 const IGNORED_COLLECTION_NAMES = [
   'sessions',
   'sessions',
@@ -45,8 +48,8 @@ class ExportArchiveDataPage extends React.Component {
     // TODO:: use apiv3.get
     // TODO:: use apiv3.get
     // eslint-disable-next-line no-unused-vars
     // eslint-disable-next-line no-unused-vars
     const [{ collections }, { status }] = await Promise.all([
     const [{ collections }, { status }] = await Promise.all([
-      this.props.appContainer.apiGet('/v3/mongo/collections', {}),
-      this.props.appContainer.apiGet('/v3/export/status', {}),
+      apiGet('/v3/mongo/collections', {}),
+      apiGet('/v3/export/status', {}),
     ]);
     ]);
     // TODO: toastSuccess, toastError
     // TODO: toastSuccess, toastError
 
 
@@ -118,7 +121,7 @@ class ExportArchiveDataPage extends React.Component {
 
 
   async onZipFileStatRemove(fileName) {
   async onZipFileStatRemove(fileName) {
     try {
     try {
-      await this.props.appContainer.apiDelete(`/v3/export/${fileName}`, {});
+      await apiDelete(`/v3/export/${fileName}`, {});
 
 
       this.setState((prevState) => {
       this.setState((prevState) => {
         return {
         return {

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

@@ -1,20 +1,22 @@
 import React from 'react';
 import React from 'react';
+
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } 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';
 import GrowiArchiveImportOption from '~/models/admin/growi-archive-import-option';
 import ImportOptionForPages from '~/models/admin/import-option-for-pages';
 import ImportOptionForPages from '~/models/admin/import-option-for-pages';
 import ImportOptionForRevisions from '~/models/admin/import-option-for-revisions';
 import ImportOptionForRevisions from '~/models/admin/import-option-for-revisions';
 
 
 import { withUnstatedContainers } from '../../../UnstatedUtils';
 import { withUnstatedContainers } from '../../../UnstatedUtils';
-import AppContainer from '~/client/services/AppContainer';
-import AdminSocketIoContainer from '~/client/services/AdminSocketIoContainer';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
 
 
 
-import ImportCollectionItem, { DEFAULT_MODE, MODE_RESTRICTED_COLLECTION } from './ImportCollectionItem';
-import ImportCollectionConfigurationModal from './ImportCollectionConfigurationModal';
 import ErrorViewer from './ErrorViewer';
 import ErrorViewer from './ErrorViewer';
+import ImportCollectionConfigurationModal from './ImportCollectionConfigurationModal';
+import ImportCollectionItem, { DEFAULT_MODE, MODE_RESTRICTED_COLLECTION } from './ImportCollectionItem';
 
 
 
 
 const GROUPS_PAGE = [
 const GROUPS_PAGE = [
@@ -300,8 +302,8 @@ class ImportForm extends React.Component {
     });
     });
 
 
     try {
     try {
-      // TODO: use appContainer.apiv3.post
-      await appContainer.apiv3Post('/import', {
+      // TODO: use apiv3Post
+      await apiv3Post('/import', {
         fileName,
         fileName,
         collections: Array.from(selectedCollections),
         collections: Array.from(selectedCollections),
         optionsMap,
         optionsMap,

+ 5 - 2
packages/app/src/components/Admin/ImportData/GrowiArchive/UploadForm.jsx

@@ -1,10 +1,13 @@
 import React from 'react';
 import React from 'react';
+
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
-import { withUnstatedContainers } from '../../../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import { toastError } from '~/client/util/apiNotification';
 import { toastError } from '~/client/util/apiNotification';
+import { apiv3Post } from '~/client/util/apiv3-client';
+
+import { withUnstatedContainers } from '../../../UnstatedUtils';
 
 
 class UploadForm extends React.Component {
 class UploadForm extends React.Component {
 
 
@@ -32,7 +35,7 @@ class UploadForm extends React.Component {
     formData.append('file', this.inputRef.current.files[0]);
     formData.append('file', this.inputRef.current.files[0]);
 
 
     try {
     try {
-      const { data } = await this.props.appContainer.apiv3Post('/import/upload', formData);
+      const { data } = await apiv3Post('/import/upload', formData);
       // TODO: toastSuccess, toastError
       // TODO: toastSuccess, toastError
       this.props.onUpload(data);
       this.props.onUpload(data);
     }
     }

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

@@ -1,14 +1,17 @@
 import React, { Fragment } from 'react';
 import React, { Fragment } from 'react';
+
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 import * as toastr from 'toastr';
 import * as toastr from 'toastr';
 
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
+import { apiv3Delete, apiv3Get } from '~/client/util/apiv3-client';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
 // import { toastSuccess, toastError } from '~/client/util/apiNotification';
 // import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
 
-import UploadForm from './GrowiArchive/UploadForm';
 import ImportForm from './GrowiArchive/ImportForm';
 import ImportForm from './GrowiArchive/ImportForm';
+import UploadForm from './GrowiArchive/UploadForm';
 
 
 class GrowiArchiveSection extends React.Component {
 class GrowiArchiveSection extends React.Component {
 
 
@@ -32,7 +35,7 @@ class GrowiArchiveSection extends React.Component {
 
 
   async componentWillMount() {
   async componentWillMount() {
     // get uploaded file status
     // get uploaded file status
-    const res = await this.props.appContainer.apiv3Get('/import/status');
+    const res = await apiv3Get('/import/status');
 
 
     if (res.data.zipFileStat != null) {
     if (res.data.zipFileStat != null) {
       const { fileName, innerFileStats } = res.data.zipFileStat;
       const { fileName, innerFileStats } = res.data.zipFileStat;
@@ -55,7 +58,7 @@ class GrowiArchiveSection extends React.Component {
   async discardData() {
   async discardData() {
     try {
     try {
       const { fileName } = this.state;
       const { fileName } = this.state;
-      await this.props.appContainer.apiv3Delete('/import/all');
+      await apiv3Delete('/import/all');
       this.resetState();
       this.resetState();
 
 
       // TODO: toastSuccess, toastError
       // TODO: toastSuccess, toastError

+ 8 - 5
packages/app/src/components/Admin/Notification/GlobalNotificationList.jsx

@@ -1,18 +1,21 @@
 import React from 'react';
 import React from 'react';
+
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
-import loggerFactory from '~/utils/logger';
 
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
+import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
+import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { apiv3Put } from '~/client/util/apiv3-client';
+import loggerFactory from '~/utils/logger';
 
 
-import AppContainer from '~/client/services/AppContainer';
-import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
+import { withUnstatedContainers } from '../../UnstatedUtils';
 
 
 import NotificationDeleteModal from './NotificationDeleteModal';
 import NotificationDeleteModal from './NotificationDeleteModal';
 import NotificationTypeIcon from './NotificationTypeIcon';
 import NotificationTypeIcon from './NotificationTypeIcon';
 
 
+
 const logger = loggerFactory('growi:GolobalNotificationList');
 const logger = loggerFactory('growi:GolobalNotificationList');
 
 
 class GlobalNotificationList extends React.Component {
 class GlobalNotificationList extends React.Component {
@@ -34,7 +37,7 @@ class GlobalNotificationList extends React.Component {
     const { t } = this.props;
     const { t } = this.props;
     const isEnabled = !notification.isEnabled;
     const isEnabled = !notification.isEnabled;
     try {
     try {
-      await this.props.appContainer.apiv3.put(`/notification-setting/global-notification/${notification._id}/enabled`, {
+      await apiv3Put(`/notification-setting/global-notification/${notification._id}/enabled`, {
         isEnabled,
         isEnabled,
       });
       });
       toastSuccess(t('notification_setting.toggle_notification', { path: notification.triggerPath }));
       toastSuccess(t('notification_setting.toggle_notification', { path: notification.triggerPath }));

+ 10 - 6
packages/app/src/components/Admin/Notification/ManageGlobalNotification.jsx

@@ -1,16 +1,20 @@
 import React from 'react';
 import React from 'react';
+
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 
 
+import AppContainer from '~/client/services/AppContainer';
+import { toastError } from '~/client/util/apiNotification';
+import { apiv3Post, apiv3Put } from '~/client/util/apiv3-client';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-import { toastError } from '~/client/util/apiNotification';
 
 
-import TriggerEventCheckBox from './TriggerEventCheckBox';
-import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-import AppContainer from '~/client/services/AppContainer';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
+import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
+
+import TriggerEventCheckBox from './TriggerEventCheckBox';
+
 
 
 const logger = loggerFactory('growi:manageGlobalNotification');
 const logger = loggerFactory('growi:manageGlobalNotification');
 
 
@@ -81,10 +85,10 @@ class ManageGlobalNotification extends React.Component {
 
 
     try {
     try {
       if (this.state.globalNotificationId != null) {
       if (this.state.globalNotificationId != null) {
-        await this.props.appContainer.apiv3.put(`/notification-setting/global-notification/${this.state.globalNotificationId}`, requestParams);
+        await apiv3Put(`/notification-setting/global-notification/${this.state.globalNotificationId}`, requestParams);
       }
       }
       else {
       else {
-        await this.props.appContainer.apiv3.post('/notification-setting/global-notification', requestParams);
+        await apiv3Post('/notification-setting/global-notification', requestParams);
       }
       }
       window.location.href = urljoin(window.location.origin, '/admin/notification#global-notification');
       window.location.href = urljoin(window.location.origin, '/admin/notification#global-notification');
     }
     }

+ 7 - 5
packages/app/src/components/Admin/Security/GoogleSecuritySettingContents.jsx

@@ -1,14 +1,16 @@
 /* eslint-disable react/no-danger */
 /* eslint-disable react/no-danger */
 import React from 'react';
 import React from 'react';
+
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
 
-import AppContainer from '~/client/services/AppContainer';
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminGoogleSecurityContainer from '~/client/services/AdminGoogleSecurityContainer';
 import AdminGoogleSecurityContainer from '~/client/services/AdminGoogleSecurityContainer';
+import AppContainer from '~/client/services/AppContainer';
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
 
 
 class GoogleSecurityManagementContents extends React.Component {
 class GoogleSecurityManagementContents extends React.Component {
 
 
@@ -135,8 +137,8 @@ class GoogleSecurityManagementContents extends React.Component {
                     id="bindByUserNameGoogle"
                     id="bindByUserNameGoogle"
                     className="custom-control-input"
                     className="custom-control-input"
                     type="checkbox"
                     type="checkbox"
-                    checked={adminGoogleSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser || false}
-                    onChange={() => { adminGoogleSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
+                    checked={adminGoogleSecurityContainer.state.isSameEmailTreatedAsIdenticalUser || false}
+                    onChange={() => { adminGoogleSecurityContainer.switchIsSameEmailTreatedAsIdenticalUser() }}
                   />
                   />
                   <label
                   <label
                     className="custom-control-label"
                     className="custom-control-label"

+ 7 - 5
packages/app/src/components/Admin/Security/LdapAuthTest.jsx

@@ -1,13 +1,15 @@
 import React from 'react';
 import React from 'react';
+
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
-import loggerFactory from '~/utils/logger';
 
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
+import AdminLdapSecurityContainer from '~/client/services/AdminLdapSecurityContainer';
+import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { apiPost } from '~/client/util/apiv1-client';
+import loggerFactory from '~/utils/logger';
 
 
-import AppContainer from '~/client/services/AppContainer';
-import AdminLdapSecurityContainer from '~/client/services/AdminLdapSecurityContainer';
+import { withUnstatedContainers } from '../../UnstatedUtils';
 
 
 const logger = loggerFactory('growi:security:AdminLdapSecurityContainer');
 const logger = loggerFactory('growi:security:AdminLdapSecurityContainer');
 
 
@@ -41,7 +43,7 @@ class LdapAuthTest extends React.Component {
    */
    */
   async testLdapCredentials() {
   async testLdapCredentials() {
     try {
     try {
-      const response = await this.props.appContainer.apiPost('/login/testLdap', {
+      const response = await apiPost('/login/testLdap', {
         loginForm: {
         loginForm: {
           username: this.props.username,
           username: this.props.username,
           password: this.props.password,
           password: this.props.password,

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

@@ -1,17 +1,19 @@
 import React, { Fragment } from 'react';
 import React, { Fragment } from 'react';
+
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
+import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
+import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { apiv3Delete } from '~/client/util/apiv3-client';
 
 
 import PaginationWrapper from '../../PaginationWrapper';
 import PaginationWrapper from '../../PaginationWrapper';
+import ShareLinkList from '../../ShareLink/ShareLinkList';
+import { withUnstatedContainers } from '../../UnstatedUtils';
 
 
-import AppContainer from '~/client/services/AppContainer';
-import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 
 
 import DeleteAllShareLinksModal from './DeleteAllShareLinksModal';
 import DeleteAllShareLinksModal from './DeleteAllShareLinksModal';
-import ShareLinkList from '../../ShareLink/ShareLinkList';
 
 
 
 
 const Pager = (props) => {
 const Pager = (props) => {
@@ -80,7 +82,7 @@ class ShareLinkSetting extends React.Component {
     const { t, appContainer } = this.props;
     const { t, appContainer } = this.props;
 
 
     try {
     try {
-      const res = await appContainer.apiv3Delete('/share-links/all');
+      const res = await apiv3Delete('/share-links/all');
       const { deletedCount } = res.data;
       const { deletedCount } = res.data;
       toastSuccess(t('toaster.remove_share_link', { count: deletedCount }));
       toastSuccess(t('toaster.remove_share_link', { count: deletedCount }));
     }
     }
@@ -95,7 +97,7 @@ class ShareLinkSetting extends React.Component {
     const { shareLinksActivePage } = adminGeneralSecurityContainer.state;
     const { shareLinksActivePage } = adminGeneralSecurityContainer.state;
 
 
     try {
     try {
-      const res = await appContainer.apiv3Delete(`/share-links/${shareLinkId}`);
+      const res = await apiv3Delete(`/share-links/${shareLinkId}`);
       const { deletedShareLink } = res.data;
       const { deletedShareLink } = res.data;
       toastSuccess(t('toaster.remove_share_link_success', { shareLinkId: deletedShareLink._id }));
       toastSuccess(t('toaster.remove_share_link_success', { shareLinkId: deletedShareLink._id }));
     }
     }

+ 12 - 8
packages/app/src/components/Admin/SlackIntegration/CustomBotWithProxySettings.jsx

@@ -1,15 +1,19 @@
 import React, { useState, useEffect, useCallback } from 'react';
 import React, { useState, useEffect, useCallback } from 'react';
-import { useTranslation } from 'react-i18next';
+
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
+import { useTranslation } from 'react-i18next';
 
 
-import loggerFactory from '~/utils/logger';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
-import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { apiv3Delete, apiv3Put } from '~/client/util/apiv3-client';
+import loggerFactory from '~/utils/logger';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
+
 import CustomBotWithProxyConnectionStatus from './CustomBotWithProxyConnectionStatus';
 import CustomBotWithProxyConnectionStatus from './CustomBotWithProxyConnectionStatus';
-import WithProxyAccordions from './WithProxyAccordions';
 import DeleteSlackBotSettingsModal from './DeleteSlackBotSettingsModal';
 import DeleteSlackBotSettingsModal from './DeleteSlackBotSettingsModal';
 import { SlackAppIntegrationControl } from './SlackAppIntegrationControl';
 import { SlackAppIntegrationControl } from './SlackAppIntegrationControl';
+import WithProxyAccordions from './WithProxyAccordions';
 
 
 const logger = loggerFactory('growi:cli:SlackIntegration:CustomBotWithProxySettings');
 const logger = loggerFactory('growi:cli:SlackIntegration:CustomBotWithProxySettings');
 
 
@@ -42,7 +46,7 @@ const CustomBotWithProxySettings = (props) => {
     }
     }
 
 
     try {
     try {
-      await appContainer.apiv3.put(`/slack-integration-settings/slack-app-integrations/${slackIntegrationToChange._id}/make-primary`);
+      await apiv3Put(`/slack-integration-settings/slack-app-integrations/${slackIntegrationToChange._id}/make-primary`);
       if (onPrimaryUpdated != null) {
       if (onPrimaryUpdated != null) {
         onPrimaryUpdated();
         onPrimaryUpdated();
       }
       }
@@ -52,11 +56,11 @@ const CustomBotWithProxySettings = (props) => {
       toastError(err, 'Failed to change isPrimary');
       toastError(err, 'Failed to change isPrimary');
       logger.error('Failed to change isPrimary', err);
       logger.error('Failed to change isPrimary', err);
     }
     }
-  }, [appContainer.apiv3, t, onPrimaryUpdated]);
+  }, [t, onPrimaryUpdated]);
 
 
   const deleteSlackAppIntegrationHandler = async() => {
   const deleteSlackAppIntegrationHandler = async() => {
     try {
     try {
-      await appContainer.apiv3.delete(`/slack-integration-settings/slack-app-integrations/${integrationIdToDelete}`);
+      await apiv3Delete(`/slack-integration-settings/slack-app-integrations/${integrationIdToDelete}`);
       if (props.onDeleteSlackAppIntegration != null) {
       if (props.onDeleteSlackAppIntegration != null) {
         props.onDeleteSlackAppIntegration();
         props.onDeleteSlackAppIntegration();
       }
       }
@@ -70,7 +74,7 @@ const CustomBotWithProxySettings = (props) => {
 
 
   const updateProxyUri = async() => {
   const updateProxyUri = async() => {
     try {
     try {
-      await appContainer.apiv3.put('/slack-integration-settings/proxy-uri', {
+      await apiv3Put('/slack-integration-settings/proxy-uri', {
         proxyUri: newProxyServerUri,
         proxyUri: newProxyServerUri,
       });
       });
       toastSuccess(t('toaster.update_successed', { target: 'Proxy URL' }));
       toastSuccess(t('toaster.update_successed', { target: 'Proxy URL' }));

+ 8 - 4
packages/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySecretTokenSection.jsx

@@ -1,10 +1,14 @@
 import React, { useState, useEffect } from 'react';
 import React, { useState, useEffect } from 'react';
-import { useTranslation } from 'react-i18next';
+
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
-import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
+import { useTranslation } from 'react-i18next';
+
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
-import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { apiv3Put } from '~/client/util/apiv3-client';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
+import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 
 
 
 const CustomBotWithoutProxySecretTokenSection = (props) => {
 const CustomBotWithoutProxySecretTokenSection = (props) => {
@@ -26,7 +30,7 @@ const CustomBotWithoutProxySecretTokenSection = (props) => {
 
 
   const updatedSecretToken = async() => {
   const updatedSecretToken = async() => {
     try {
     try {
-      await appContainer.apiv3.put('/slack-integration-settings/without-proxy/update-settings', {
+      await apiv3Put('/slack-integration-settings/without-proxy/update-settings', {
         slackSigningSecret: inputSigningSecret,
         slackSigningSecret: inputSigningSecret,
         slackBotToken: inputBotToken,
         slackBotToken: inputBotToken,
       });
       });

+ 9 - 5
packages/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx

@@ -1,13 +1,18 @@
 import React, { useState } from 'react';
 import React, { useState } from 'react';
+
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
-import Accordion from '../Common/Accordion';
+
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
+import { apiv3Post } from '~/client/util/apiv3-client';
+
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
-import MessageBasedOnConnection from './MessageBasedOnConnection';
+import Accordion from '../Common/Accordion';
+
 import CustomBotWithoutProxySecretTokenSection from './CustomBotWithoutProxySecretTokenSection';
 import CustomBotWithoutProxySecretTokenSection from './CustomBotWithoutProxySecretTokenSection';
-import { addLogs } from './slak-integration-util';
 import ManageCommandsProcessWithoutProxy from './ManageCommandsProcessWithoutProxy';
 import ManageCommandsProcessWithoutProxy from './ManageCommandsProcessWithoutProxy';
+import MessageBasedOnConnection from './MessageBasedOnConnection';
+import { addLogs } from './slak-integration-util';
 
 
 
 
 export const botInstallationStep = {
 export const botInstallationStep = {
@@ -34,7 +39,7 @@ const CustomBotWithoutProxySettingsAccordion = (props) => {
 
 
   const testConnection = async() => {
   const testConnection = async() => {
     try {
     try {
-      await appContainer.apiv3.post('/slack-integration-settings/without-proxy/test', { channel: testChannel });
+      await apiv3Post('/slack-integration-settings/without-proxy/test', { channel: testChannel });
       setIsLatestConnectionSuccess(true);
       setIsLatestConnectionSuccess(true);
       if (onTestConnectionInvoked != null) {
       if (onTestConnectionInvoked != null) {
         onTestConnectionInvoked();
         onTestConnectionInvoked();
@@ -130,7 +135,6 @@ const CustomBotWithoutProxySettingsAccordion = (props) => {
         <ManageCommandsProcessWithoutProxy
         <ManageCommandsProcessWithoutProxy
           commandPermission={commandPermission}
           commandPermission={commandPermission}
           eventActionsPermission={eventActionsPermission}
           eventActionsPermission={eventActionsPermission}
-          apiv3Put={props.appContainer.apiv3.put}
         />
         />
       </Accordion>
       </Accordion>
       <Accordion
       <Accordion

+ 5 - 3
packages/app/src/components/Admin/SlackIntegration/ManageCommandsProcess.jsx

@@ -1,7 +1,10 @@
 import React, { useCallback, useState } from 'react';
 import React, { useCallback, useState } from 'react';
+
+import { defaultSupportedCommandsNameForBroadcastUse, defaultSupportedCommandsNameForSingleUse, defaultSupportedSlackEventActions } from '@growi/slack';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
-import { defaultSupportedCommandsNameForBroadcastUse, defaultSupportedCommandsNameForSingleUse, defaultSupportedSlackEventActions } from '@growi/slack';
+
+import { apiv3Put } from '~/client/util/apiv3-client';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { toastSuccess, toastError } from '../../../client/util/apiNotification';
 import { toastSuccess, toastError } from '../../../client/util/apiNotification';
@@ -170,7 +173,7 @@ PermissionSettingForEachPermissionTypeComponent.propTypes = {
 
 
 // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 const ManageCommandsProcess = ({
 const ManageCommandsProcess = ({
-  apiv3Put, slackAppIntegrationId, permissionsForBroadcastUseCommands, permissionsForSingleUseCommands, permissionsForSlackEventActions,
+  slackAppIntegrationId, permissionsForBroadcastUseCommands, permissionsForSingleUseCommands, permissionsForSlackEventActions,
 }) => {
 }) => {
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
@@ -401,7 +404,6 @@ const ManageCommandsProcess = ({
 };
 };
 
 
 ManageCommandsProcess.propTypes = {
 ManageCommandsProcess.propTypes = {
-  apiv3Put: PropTypes.func,
   slackAppIntegrationId: PropTypes.string.isRequired,
   slackAppIntegrationId: PropTypes.string.isRequired,
   permissionsForBroadcastUseCommands: PropTypes.object.isRequired,
   permissionsForBroadcastUseCommands: PropTypes.object.isRequired,
   permissionsForSingleUseCommands: PropTypes.object.isRequired,
   permissionsForSingleUseCommands: PropTypes.object.isRequired,

+ 5 - 3
packages/app/src/components/Admin/SlackIntegration/ManageCommandsProcessWithoutProxy.jsx

@@ -1,7 +1,10 @@
 import React, { useCallback, useEffect, useState } from 'react';
 import React, { useCallback, useEffect, useState } from 'react';
+
+import { defaultSupportedCommandsNameForBroadcastUse, defaultSupportedCommandsNameForSingleUse, defaultSupportedSlackEventActions } from '@growi/slack';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
-import { defaultSupportedCommandsNameForBroadcastUse, defaultSupportedCommandsNameForSingleUse, defaultSupportedSlackEventActions } from '@growi/slack';
+
+import { apiv3Put } from '~/client/util/apiv3-client';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { toastSuccess, toastError } from '../../../client/util/apiNotification';
 import { toastSuccess, toastError } from '../../../client/util/apiNotification';
@@ -153,7 +156,7 @@ SinglePermissionSettingComponent.propTypes = {
 
 
 
 
 // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-const ManageCommandsProcessWithoutProxy = ({ apiv3Put, commandPermission, eventActionsPermission }) => {
+const ManageCommandsProcessWithoutProxy = ({ commandPermission, eventActionsPermission }) => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const [editingCommandPermission, setEditingCommandPermission] = useState({});
   const [editingCommandPermission, setEditingCommandPermission] = useState({});
   const [editingEventActionsPermission, setEditingEventActionsPermission] = useState({});
   const [editingEventActionsPermission, setEditingEventActionsPermission] = useState({});
@@ -267,7 +270,6 @@ const ManageCommandsProcessWithoutProxy = ({ apiv3Put, commandPermission, eventA
 };
 };
 
 
 ManageCommandsProcessWithoutProxy.propTypes = {
 ManageCommandsProcessWithoutProxy.propTypes = {
-  apiv3Put: PropTypes.func,
   commandPermission: PropTypes.object,
   commandPermission: PropTypes.object,
   eventActionsPermission: PropTypes.object,
   eventActionsPermission: PropTypes.object,
 };
 };

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

@@ -1,17 +1,22 @@
 import React, { useState, useEffect, useCallback } from 'react';
 import React, { useState, useEffect, useCallback } from 'react';
+
+import { SlackbotType } from '@growi/slack';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
-import { SlackbotType } from '@growi/slack';
 
 
-import loggerFactory from '~/utils/logger';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
-import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { apiv3Delete, apiv3Put } from '~/client/util/apiv3-client';
+import loggerFactory from '~/utils/logger';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
+
+
 import CustomBotWithProxyConnectionStatus from './CustomBotWithProxyConnectionStatus';
 import CustomBotWithProxyConnectionStatus from './CustomBotWithProxyConnectionStatus';
-import WithProxyAccordions from './WithProxyAccordions';
 import DeleteSlackBotSettingsModal from './DeleteSlackBotSettingsModal';
 import DeleteSlackBotSettingsModal from './DeleteSlackBotSettingsModal';
 import { SlackAppIntegrationControl } from './SlackAppIntegrationControl';
 import { SlackAppIntegrationControl } from './SlackAppIntegrationControl';
+import WithProxyAccordions from './WithProxyAccordions';
 
 
 const logger = loggerFactory('growi:cli:SlackIntegration:OfficialBotSettings');
 const logger = loggerFactory('growi:cli:SlackIntegration:OfficialBotSettings');
 
 
@@ -38,7 +43,7 @@ const OfficialBotSettings = (props) => {
     }
     }
 
 
     try {
     try {
-      await appContainer.apiv3.put(`/slack-integration-settings/slack-app-integrations/${slackIntegrationToChange._id}/make-primary`);
+      await apiv3Put(`/slack-integration-settings/slack-app-integrations/${slackIntegrationToChange._id}/make-primary`);
       if (onPrimaryUpdated != null) {
       if (onPrimaryUpdated != null) {
         onPrimaryUpdated();
         onPrimaryUpdated();
       }
       }
@@ -48,10 +53,10 @@ const OfficialBotSettings = (props) => {
       toastError(err, 'Failed to change isPrimary');
       toastError(err, 'Failed to change isPrimary');
       logger.error('Failed to change isPrimary', err);
       logger.error('Failed to change isPrimary', err);
     }
     }
-  }, [appContainer.apiv3, t, onPrimaryUpdated]);
+  }, [t, onPrimaryUpdated]);
 
 
   const deleteSlackAppIntegrationHandler = async() => {
   const deleteSlackAppIntegrationHandler = async() => {
-    await appContainer.apiv3.delete(`/slack-integration-settings/slack-app-integrations/${integrationIdToDelete}`);
+    await apiv3Delete(`/slack-integration-settings/slack-app-integrations/${integrationIdToDelete}`);
     try {
     try {
       if (props.onDeleteSlackAppIntegration != null) {
       if (props.onDeleteSlackAppIntegration != null) {
         props.onDeleteSlackAppIntegration();
         props.onDeleteSlackAppIntegration();

+ 17 - 11
packages/app/src/components/Admin/SlackIntegration/SlackIntegration.jsx

@@ -1,19 +1,25 @@
 import React, { useState, useEffect, useCallback } from 'react';
 import React, { useState, useEffect, useCallback } from 'react';
+
+import { SlackbotType } from '@growi/slack';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
-import { SlackbotType } from '@growi/slack';
 
 
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
-import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import {
+  apiv3Delete, apiv3Get, apiv3Post, apiv3Put,
+} from '~/client/util/apiv3-client';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
 
 
-import OfficialBotSettings from './OfficialBotSettings';
-import CustomBotWithoutProxySettings from './CustomBotWithoutProxySettings';
-import CustomBotWithProxySettings from './CustomBotWithProxySettings';
-import ConfirmBotChangeModal from './ConfirmBotChangeModal';
 import BotTypeCard from './BotTypeCard';
 import BotTypeCard from './BotTypeCard';
+import ConfirmBotChangeModal from './ConfirmBotChangeModal';
+import CustomBotWithProxySettings from './CustomBotWithProxySettings';
+import CustomBotWithoutProxySettings from './CustomBotWithoutProxySettings';
 import DeleteSlackBotSettingsModal from './DeleteSlackBotSettingsModal';
 import DeleteSlackBotSettingsModal from './DeleteSlackBotSettingsModal';
+import OfficialBotSettings from './OfficialBotSettings';
+
 
 
 const botTypes = Object.values(SlackbotType);
 const botTypes = Object.values(SlackbotType);
 
 
@@ -40,7 +46,7 @@ const SlackIntegration = (props) => {
 
 
   const fetchSlackIntegrationData = useCallback(async() => {
   const fetchSlackIntegrationData = useCallback(async() => {
     try {
     try {
-      const { data } = await appContainer.apiv3.get('/slack-integration-settings');
+      const { data } = await apiv3Get('/slack-integration-settings');
       const {
       const {
         slackSigningSecret,
         slackSigningSecret,
         slackBotToken,
         slackBotToken,
@@ -71,11 +77,11 @@ const SlackIntegration = (props) => {
     finally {
     finally {
       setIsLoading(false);
       setIsLoading(false);
     }
     }
-  }, [appContainer.apiv3]);
+  }, []);
 
 
   const resetAllSettings = async() => {
   const resetAllSettings = async() => {
     try {
     try {
-      await appContainer.apiv3.delete('/slack-integration-settings/bot-type');
+      await apiv3Delete('/slack-integration-settings/bot-type');
       fetchSlackIntegrationData();
       fetchSlackIntegrationData();
       toastSuccess(t('admin:slack_integration.bot_all_reset_successful'));
       toastSuccess(t('admin:slack_integration.bot_all_reset_successful'));
     }
     }
@@ -86,7 +92,7 @@ const SlackIntegration = (props) => {
 
 
   const createSlackIntegrationData = async() => {
   const createSlackIntegrationData = async() => {
     try {
     try {
-      await appContainer.apiv3.post('/slack-integration-settings/slack-app-integrations');
+      await apiv3Post('/slack-integration-settings/slack-app-integrations');
       fetchSlackIntegrationData();
       fetchSlackIntegrationData();
       toastSuccess(t('admin:slack_integration.adding_slack_ws_integration_settings_successful'));
       toastSuccess(t('admin:slack_integration.adding_slack_ws_integration_settings_successful'));
     }
     }
@@ -106,7 +112,7 @@ const SlackIntegration = (props) => {
 
 
   const changeCurrentBotSettings = async(botType) => {
   const changeCurrentBotSettings = async(botType) => {
     try {
     try {
-      await appContainer.apiv3.put('/slack-integration-settings/bot-type', {
+      await apiv3Put('/slack-integration-settings/bot-type', {
         currentBotType: botType,
         currentBotType: botType,
       });
       });
       setSelectedBotType(null);
       setSelectedBotType(null);

+ 13 - 13
packages/app/src/components/Admin/SlackIntegration/WithProxyAccordions.jsx

@@ -1,21 +1,23 @@
 /* eslint-disable react/prop-types */
 /* eslint-disable react/prop-types */
 import React, { useState, useCallback } from 'react';
 import React, { useState, useCallback } from 'react';
-import PropTypes from 'prop-types';
-import { useTranslation } from 'react-i18next';
-import { CopyToClipboard } from 'react-copy-to-clipboard';
 
 
 import { SlackbotType } from '@growi/slack';
 import { SlackbotType } from '@growi/slack';
-
+import PropTypes from 'prop-types';
+import { CopyToClipboard } from 'react-copy-to-clipboard';
+import { useTranslation } from 'react-i18next';
 import { Tooltip } from 'reactstrap';
 import { Tooltip } from 'reactstrap';
+
+import AppContainer from '~/client/services/AppContainer';
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { apiv3Put } from '~/client/util/apiv3-client';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
-import AppContainer from '~/client/services/AppContainer';
 import Accordion from '../Common/Accordion';
 import Accordion from '../Common/Accordion';
-import { addLogs } from './slak-integration-util';
-import MessageBasedOnConnection from './MessageBasedOnConnection';
+
 import ManageCommandsProcess from './ManageCommandsProcess';
 import ManageCommandsProcess from './ManageCommandsProcess';
+import MessageBasedOnConnection from './MessageBasedOnConnection';
+import { addLogs } from './slak-integration-util';
 
 
 const logger = loggerFactory('growi:SlackIntegration:WithProxyAccordionsWrapper');
 const logger = loggerFactory('growi:SlackIntegration:WithProxyAccordionsWrapper');
 
 
@@ -147,7 +149,7 @@ const GeneratingTokensAndRegisteringProxyServiceProcess = withUnstatedContainers
 
 
   const regenerateTokensHandler = async() => {
   const regenerateTokensHandler = async() => {
     try {
     try {
-      await appContainer.apiv3.put(`/slack-integration-settings/slack-app-integrations/${slackAppIntegrationId}/regenerate-tokens`);
+      await apiv3Put(`/slack-integration-settings/slack-app-integrations/${slackAppIntegrationId}/regenerate-tokens`);
       if (props.onUpdateTokens != null) {
       if (props.onUpdateTokens != null) {
         props.onUpdateTokens();
         props.onUpdateTokens();
       }
       }
@@ -342,7 +344,6 @@ const WithProxyAccordions = (props) => {
     '③': {
     '③': {
       title: 'manage_permission',
       title: 'manage_permission',
       content: <ManageCommandsProcess
       content: <ManageCommandsProcess
-        apiv3Put={props.appContainer.apiv3.put}
         slackAppIntegrationId={props.slackAppIntegrationId}
         slackAppIntegrationId={props.slackAppIntegrationId}
         permissionsForBroadcastUseCommands={props.permissionsForBroadcastUseCommands}
         permissionsForBroadcastUseCommands={props.permissionsForBroadcastUseCommands}
         permissionsForSingleUseCommands={props.permissionsForSingleUseCommands}
         permissionsForSingleUseCommands={props.permissionsForSingleUseCommands}
@@ -352,7 +353,7 @@ const WithProxyAccordions = (props) => {
     '④': {
     '④': {
       title: 'test_connection',
       title: 'test_connection',
       content: <TestProcess
       content: <TestProcess
-        apiv3Post={props.appContainer.apiv3.post}
+        apiv3Post={props.apiv3Post}
         slackAppIntegrationId={props.slackAppIntegrationId}
         slackAppIntegrationId={props.slackAppIntegrationId}
         onSubmitForm={submitForm}
         onSubmitForm={submitForm}
         onSubmitFormFailed={submitFormFailed}
         onSubmitFormFailed={submitFormFailed}
@@ -387,7 +388,6 @@ const WithProxyAccordions = (props) => {
     '⑤': {
     '⑤': {
       title: 'manage_permission',
       title: 'manage_permission',
       content: <ManageCommandsProcess
       content: <ManageCommandsProcess
-        apiv3Put={props.appContainer.apiv3.put}
         slackAppIntegrationId={props.slackAppIntegrationId}
         slackAppIntegrationId={props.slackAppIntegrationId}
         permissionsForBroadcastUseCommands={props.permissionsForBroadcastUseCommands}
         permissionsForBroadcastUseCommands={props.permissionsForBroadcastUseCommands}
         permissionsForSingleUseCommands={props.permissionsForSingleUseCommands}
         permissionsForSingleUseCommands={props.permissionsForSingleUseCommands}
@@ -397,7 +397,7 @@ const WithProxyAccordions = (props) => {
     '⑥': {
     '⑥': {
       title: 'test_connection',
       title: 'test_connection',
       content: <TestProcess
       content: <TestProcess
-        apiv3Post={props.appContainer.apiv3.post}
+        apiv3Post={props.apiv3Post}
         slackAppIntegrationId={props.slackAppIntegrationId}
         slackAppIntegrationId={props.slackAppIntegrationId}
         onSubmitForm={submitForm}
         onSubmitForm={submitForm}
         onSubmitFormFailed={submitFormFailed}
         onSubmitFormFailed={submitFormFailed}

+ 7 - 4
packages/app/src/components/Admin/UserGroupDetail/UserGroupPageList.jsx

@@ -1,13 +1,16 @@
 import React, { Fragment } from 'react';
 import React, { Fragment } from 'react';
+
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
+import AdminUserGroupDetailContainer from '~/client/services/AdminUserGroupDetailContainer';
+import AppContainer from '~/client/services/AppContainer';
+import { toastError } from '~/client/util/apiNotification';
+import { apiv3Get } from '~/client/util/apiv3-client';
+
 import PageListItemS from '../../PageList/PageListItemS';
 import PageListItemS from '../../PageList/PageListItemS';
 import PaginationWrapper from '../../PaginationWrapper';
 import PaginationWrapper from '../../PaginationWrapper';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
-import AppContainer from '~/client/services/AppContainer';
-import AdminUserGroupDetailContainer from '~/client/services/AdminUserGroupDetailContainer';
-import { toastError } from '~/client/util/apiNotification';
 
 
 class UserGroupPageList extends React.Component {
 class UserGroupPageList extends React.Component {
 
 
@@ -33,7 +36,7 @@ class UserGroupPageList extends React.Component {
     const offset = (pageNum - 1) * limit;
     const offset = (pageNum - 1) * limit;
 
 
     try {
     try {
-      const res = await this.props.appContainer.apiv3.get(`/user-groups/${this.props.adminUserGroupDetailContainer.state.userGroup._id}/pages`, {
+      const res = await apiv3Get(`/user-groups/${this.props.adminUserGroupDetailContainer.state.userGroup._id}/pages`, {
         limit,
         limit,
         offset,
         offset,
       });
       });

+ 5 - 2
packages/app/src/components/Admin/Users/PasswordResetModal.jsx

@@ -1,13 +1,16 @@
 import React from 'react';
 import React from 'react';
+
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 import {
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
+import AppContainer from '~/client/services/AppContainer';
 import { toastError } from '~/client/util/apiNotification';
 import { toastError } from '~/client/util/apiNotification';
+import { apiv3Put } from '~/client/util/apiv3-client';
+
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
-import AppContainer from '~/client/services/AppContainer';
 
 
 class PasswordResetModal extends React.Component {
 class PasswordResetModal extends React.Component {
 
 
@@ -25,7 +28,7 @@ class PasswordResetModal extends React.Component {
   async resetPassword() {
   async resetPassword() {
     const { t, appContainer, userForPasswordResetModal } = this.props;
     const { t, appContainer, userForPasswordResetModal } = this.props;
     try {
     try {
-      const res = await appContainer.apiv3Put('/users/reset-password', { id: userForPasswordResetModal._id });
+      const res = await apiv3Put('/users/reset-password', { id: userForPasswordResetModal._id });
       const { newPassword } = res.data;
       const { newPassword } = res.data;
       this.setState({ temporaryPassword: newPassword, isPasswordResetDone: true });
       this.setState({ temporaryPassword: newPassword, isPasswordResetDone: true });
     }
     }

+ 8 - 4
packages/app/src/components/Admin/Users/SendInvitationEmailButton.jsx

@@ -1,9 +1,13 @@
 import React from 'react';
 import React from 'react';
-import { useTranslation } from 'react-i18next';
+
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
-import AppContainer from '~/client/services/AppContainer';
+import { useTranslation } from 'react-i18next';
+
 import AdminUsersContainer from '~/client/services/AdminUsersContainer';
 import AdminUsersContainer from '~/client/services/AdminUsersContainer';
+import AppContainer from '~/client/services/AppContainer';
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { apiv3Put } from '~/client/util/apiv3-client';
+
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
 
 const SendInvitationEmailButton = (props) => {
 const SendInvitationEmailButton = (props) => {
@@ -16,7 +20,7 @@ const SendInvitationEmailButton = (props) => {
 
 
   const onClickSendInvitationEmailButton = async() => {
   const onClickSendInvitationEmailButton = async() => {
     try {
     try {
-      const res = await appContainer.apiv3Put('users/send-invitation-email', { id: user._id });
+      const res = await apiv3Put('/users/send-invitation-email', { id: user._id });
       const { failedToSendEmail } = res.data;
       const { failedToSendEmail } = res.data;
       if (failedToSendEmail == null) {
       if (failedToSendEmail == null) {
         const msg = `Email has been sent<br>・${user.email}`;
         const msg = `Email has been sent<br>・${user.email}`;

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

@@ -1,12 +1,16 @@
 import React, { useState, useCallback } from 'react';
 import React, { useState, useCallback } from 'react';
+
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 import {
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 } from 'reactstrap';
+
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
-import { withUnstatedContainers } from './UnstatedUtils';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { apiv3Post } from '~/client/util/apiv3-client';
+
+import { withUnstatedContainers } from './UnstatedUtils';
 
 
 
 
 const ArchiveCreateModal = (props) => {
 const ArchiveCreateModal = (props) => {
@@ -56,7 +60,7 @@ const ArchiveCreateModal = (props) => {
 
 
   async function done() {
   async function done() {
     try {
     try {
-      await appContainer.apiv3Post('/page/archive', {
+      await apiv3Post('/page/archive', {
         rootPagePath: props.path,
         rootPagePath: props.path,
         isCommentDownload,
         isCommentDownload,
         isAttachmentFileDownload,
         isAttachmentFileDownload,

+ 5 - 2
packages/app/src/components/CustomNavigation/CustomNav.jsx

@@ -1,6 +1,7 @@
 import React, {
 import React, {
   useEffect, useState, useRef, useMemo, useCallback,
   useEffect, useState, useRef, useMemo, useCallback,
 } from 'react';
 } from 'react';
+
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import {
 import {
   Nav, NavItem, NavLink,
   Nav, NavItem, NavLink,
@@ -87,7 +88,7 @@ export const CustomNavTab = (props) => {
   const [sliderMarginLeft, setSliderMarginLeft] = useState(0);
   const [sliderMarginLeft, setSliderMarginLeft] = useState(0);
 
 
   const {
   const {
-    activeTab, navTabMapping, onNavSelected, hideBorderBottom, breakpointToHideInactiveTabsDown,
+    activeTab, navTabMapping, onNavSelected, hideBorderBottom, breakpointToHideInactiveTabsDown, navRightElement,
   } = props;
   } = props;
 
 
   const navTabRefs = useMemo(() => {
   const navTabRefs = useMemo(() => {
@@ -149,7 +150,7 @@ export const CustomNavTab = (props) => {
 
 
   return (
   return (
     <div className="grw-custom-nav-tab">
     <div className="grw-custom-nav-tab">
-      <div ref={navContainer}>
+      <div ref={navContainer} className="d-flex justify-content-between">
         <Nav className="nav-title">
         <Nav className="nav-title">
           {Object.entries(navTabMapping).map(([key, value]) => {
           {Object.entries(navTabMapping).map(([key, value]) => {
 
 
@@ -169,6 +170,7 @@ export const CustomNavTab = (props) => {
             );
             );
           })}
           })}
         </Nav>
         </Nav>
+        {navRightElement}
       </div>
       </div>
       <hr className="my-0 grw-nav-slide-hr border-none" style={{ width: `${sliderWidth}%`, marginLeft: `${sliderMarginLeft}%` }} />
       <hr className="my-0 grw-nav-slide-hr border-none" style={{ width: `${sliderWidth}%`, marginLeft: `${sliderMarginLeft}%` }} />
       { !hideBorderBottom && <hr className="my-0 border-top-0 border-bottom" /> }
       { !hideBorderBottom && <hr className="my-0 border-top-0 border-bottom" /> }
@@ -183,6 +185,7 @@ CustomNavTab.propTypes = {
   onNavSelected: PropTypes.func,
   onNavSelected: PropTypes.func,
   hideBorderBottom: PropTypes.bool,
   hideBorderBottom: PropTypes.bool,
   breakpointToHideInactiveTabsDown: PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl']),
   breakpointToHideInactiveTabsDown: PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl']),
+  navRightElement: PropTypes.node,
 };
 };
 
 
 CustomNavTab.defaultProps = {
 CustomNavTab.defaultProps = {

+ 4 - 1
packages/app/src/components/CustomNavigation/CustomNavAndContents.jsx

@@ -1,4 +1,5 @@
 import React, { useState } from 'react';
 import React, { useState } from 'react';
+
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 import CustomNav, { CustomNavTab, CustomNavDropdown } from './CustomNav';
 import CustomNav, { CustomNavTab, CustomNavDropdown } from './CustomNav';
@@ -7,7 +8,7 @@ import CustomTabContent from './CustomTabContent';
 
 
 const CustomNavAndContents = (props) => {
 const CustomNavAndContents = (props) => {
   const {
   const {
-    navTabMapping, defaultTabIndex, navigationMode, tabContentClasses, breakpointToHideInactiveTabsDown,
+    navTabMapping, defaultTabIndex, navigationMode, tabContentClasses, breakpointToHideInactiveTabsDown, navRightElement,
   } = props;
   } = props;
   const [activeTab, setActiveTab] = useState(Object.keys(props.navTabMapping)[defaultTabIndex || 0]);
   const [activeTab, setActiveTab] = useState(Object.keys(props.navTabMapping)[defaultTabIndex || 0]);
 
 
@@ -31,6 +32,7 @@ const CustomNavAndContents = (props) => {
         navTabMapping={navTabMapping}
         navTabMapping={navTabMapping}
         onNavSelected={setActiveTab}
         onNavSelected={setActiveTab}
         breakpointToHideInactiveTabsDown={breakpointToHideInactiveTabsDown}
         breakpointToHideInactiveTabsDown={breakpointToHideInactiveTabsDown}
+        navRightElement={navRightElement}
       />
       />
       <CustomTabContent activeTab={activeTab} navTabMapping={navTabMapping} additionalClassNames={tabContentClasses} />
       <CustomTabContent activeTab={activeTab} navTabMapping={navTabMapping} additionalClassNames={tabContentClasses} />
     </>
     </>
@@ -43,6 +45,7 @@ CustomNavAndContents.propTypes = {
   navigationMode: PropTypes.oneOf(['both', 'tab', 'dropdown']),
   navigationMode: PropTypes.oneOf(['both', 'tab', 'dropdown']),
   tabContentClasses: PropTypes.arrayOf(PropTypes.string),
   tabContentClasses: PropTypes.arrayOf(PropTypes.string),
   breakpointToHideInactiveTabsDown: PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl']),
   breakpointToHideInactiveTabsDown: PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl']),
+  navRightElement: PropTypes.node,
 };
 };
 CustomNavAndContents.defaultProps = {
 CustomNavAndContents.defaultProps = {
   navigationMode: 'tab',
   navigationMode: 'tab',

+ 61 - 0
packages/app/src/components/EmptyTrashButton.tsx

@@ -0,0 +1,61 @@
+import React, { useCallback } from 'react';
+
+import { useTranslation } from 'react-i18next';
+
+import { toastSuccess } from '~/client/util/apiNotification';
+import {
+  IDataWithMeta,
+  IPageHasId,
+  IPageInfo,
+} from '~/interfaces/page';
+import { useEmptyTrashModal } from '~/stores/modal';
+import { useSWRxDescendantsPageListForCurrrentPath, useSWRxPageInfoForList } from '~/stores/page';
+
+
+const EmptyTrashButton = () => {
+  const { t } = useTranslation();
+  const { open: openEmptyTrashModal } = useEmptyTrashModal();
+  const { data: pagingResult, mutate } = useSWRxDescendantsPageListForCurrrentPath();
+
+  const pageIds = pagingResult?.items?.map(page => page._id);
+  const { injectTo } = useSWRxPageInfoForList(pageIds, true, true);
+
+  let pageWithMetas: IDataWithMeta<IPageHasId, IPageInfo>[] = [];
+
+  const convertToIDataWithMeta = (page) => {
+    return { data: page };
+  };
+
+  if (pagingResult != null) {
+    const dataWithMetas = pagingResult.items.map(page => convertToIDataWithMeta(page));
+    pageWithMetas = injectTo(dataWithMetas);
+  }
+
+  const deletablePages = pageWithMetas.filter(page => page.meta?.isAbleToDeleteCompletely);
+
+  const onEmptiedTrashHandler = useCallback(() => {
+    toastSuccess(t('empty_trash'));
+
+    mutate();
+  }, [t, mutate]);
+
+  const emptyTrashClickHandler = () => {
+    if (deletablePages.length === 0) { return }
+    openEmptyTrashModal(deletablePages, { onEmptiedTrash: onEmptiedTrashHandler, canDelepeAllPages: pagingResult?.totalCount === deletablePages.length });
+  };
+
+  return (
+    <div className="d-flex align-items-center">
+      <button
+        type="button"
+        className="btn btn-outline-secondary rounded-pill text-danger d-flex align-items-center"
+        onClick={() => emptyTrashClickHandler()}
+      >
+        <i className="icon-fw icon-trash"></i>
+        <div>{t('modal_empty.empty_the_trash')}</div>
+      </button>
+    </div>
+  );
+};
+
+export default EmptyTrashButton;

+ 0 - 71
packages/app/src/components/EmptyTrashModal.jsx

@@ -1,71 +0,0 @@
-import React, { useState } from 'react';
-import PropTypes from 'prop-types';
-
-import {
-  Modal, ModalHeader, ModalBody, ModalFooter,
-} from 'reactstrap';
-
-import { withTranslation } from 'react-i18next';
-import { withUnstatedContainers } from './UnstatedUtils';
-
-import SocketIoContainer from '~/client/services/SocketIoContainer';
-import AppContainer from '~/client/services/AppContainer';
-import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
-
-const EmptyTrashModal = (props) => {
-  const {
-    t, isOpen, onClose, appContainer, socketIoContainer,
-  } = props;
-
-  const [errs, setErrs] = useState(null);
-
-  async function emptyTrash() {
-    setErrs(null);
-
-    try {
-      await appContainer.apiv3Delete('/pages/empty-trash');
-      window.location.reload();
-    }
-    catch (err) {
-      setErrs(err);
-    }
-  }
-
-  function emptyButtonHandler() {
-    emptyTrash();
-  }
-
-  return (
-    <Modal isOpen={isOpen} toggle={onClose} className="grw-create-page">
-      <ModalHeader tag="h4" toggle={onClose} className="bg-danger text-light">
-        { t('modal_empty.empty_the_trash')}
-      </ModalHeader>
-      <ModalBody>
-        { t('modal_empty.notice')}
-      </ModalBody>
-      <ModalFooter>
-        <ApiErrorMessageList errs={errs} />
-        <button type="button" className="btn btn-danger" onClick={emptyButtonHandler}>
-          <i className="icon-trash mr-2" aria-hidden="true"></i> Empty
-        </button>
-      </ModalFooter>
-    </Modal>
-  );
-};
-
-/**
- * Wrapper component for using unstated
- */
-const EmptyTrashModalWrapper = withUnstatedContainers(EmptyTrashModal, [AppContainer, SocketIoContainer]);
-
-
-EmptyTrashModal.propTypes = {
-  t: PropTypes.func.isRequired, //  i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  socketIoContainer: PropTypes.instanceOf(SocketIoContainer),
-
-  isOpen: PropTypes.bool.isRequired,
-  onClose: PropTypes.func.isRequired,
-};
-
-export default withTranslation()(EmptyTrashModalWrapper);

+ 92 - 0
packages/app/src/components/EmptyTrashModal.tsx

@@ -0,0 +1,92 @@
+import React, {
+  useState, FC,
+} from 'react';
+
+import { useTranslation } from 'react-i18next';
+import {
+  Modal, ModalHeader, ModalBody, ModalFooter,
+} from 'reactstrap';
+
+import { apiv3Delete } from '~/client/util/apiv3-client';
+import { useEmptyTrashModal } from '~/stores/modal';
+
+import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
+
+const EmptyTrashModal: FC = () => {
+  const { t } = useTranslation();
+
+  const { data: emptyTrashModalData, close: closeEmptyTrashModal } = useEmptyTrashModal();
+
+  const isOpened = emptyTrashModalData?.isOpened ?? false;
+
+  const canDeleteAllpages = emptyTrashModalData?.opts?.canDelepeAllPages ?? false;
+
+  const [errs, setErrs] = useState<Error[] | null>(null);
+
+  async function emptyTrash() {
+    if (emptyTrashModalData == null || emptyTrashModalData.pages == null) {
+      return;
+    }
+
+    try {
+      await apiv3Delete('/pages/empty-trash');
+      const onEmptiedTrash = emptyTrashModalData.opts?.onEmptiedTrash;
+      if (onEmptiedTrash != null) {
+        onEmptiedTrash();
+      }
+      closeEmptyTrashModal();
+    }
+    catch (err) {
+      setErrs([err]);
+    }
+  }
+
+  async function emptyTrashButtonHandler() {
+    await emptyTrash();
+  }
+
+  const renderPagePaths = () => {
+    const pages = emptyTrashModalData?.pages;
+
+    if (pages != null) {
+      return pages.map(page => (
+        <p key={page.data._id} className="mb-1">
+          <code>{ page.data.path }</code>
+        </p>
+      ));
+    }
+    return <></>;
+  };
+
+  return (
+    <Modal size="lg" isOpen={isOpened} toggle={closeEmptyTrashModal} data-testid="page-delete-modal" className="grw-create-page">
+      <ModalHeader tag="h4" toggle={closeEmptyTrashModal} className="bg-danger text-light">
+        <i className="icon-fw icon-fire"></i>
+        {t('modal_empty.empty_the_trash')}
+      </ModalHeader>
+      <ModalBody>
+        <div className="form-group grw-scrollable-modal-body pb-1">
+          <label>{ t('modal_delete.deleting_page') }:</label><br />
+          {/* Todo: change the way to show path on modal when too many pages are selected */}
+          {renderPagePaths()}
+        </div>
+        {!canDeleteAllpages && t('modal_empty.not_deletable_notice')}<br />
+        {t('modal_empty.notice')}
+      </ModalBody>
+      <ModalFooter>
+        <ApiErrorMessageList errs={errs} />
+        <button
+          type="button"
+          className="btn btn-danger"
+          onClick={emptyTrashButtonHandler}
+        >
+          <i className="mr-1 icon-fire" aria-hidden="true"></i>
+          {t('modal_empty.empty_the_trash_button')}
+        </button>
+      </ModalFooter>
+    </Modal>
+
+  );
+};
+
+export default EmptyTrashModal;

+ 7 - 4
packages/app/src/components/MaintenanceModeContent.tsx

@@ -25,10 +25,13 @@ const MaintenanceModeContent = () => {
 
 
   return (
   return (
     <div className="text-left">
     <div className="text-left">
-      <p>
-        <i className="icon-arrow-right"></i>
-        <a className="btn btn-link" href="/admin">{ t('maintenance_mode.admin_page') }</a>
-      </p>
+      {currentUser?.admin
+      && (
+        <p>
+          <i className="icon-arrow-right"></i>
+          <a className="btn btn-link" href="/admin">{ t('maintenance_mode.admin_page') }</a>
+        </p>
+      )}
       {currentUser != null
       {currentUser != null
         ? (
         ? (
           <p>
           <p>

+ 2 - 1
packages/app/src/components/Me/ApiSettings.jsx

@@ -7,6 +7,7 @@ import { withTranslation } from 'react-i18next';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import PersonalContainer from '~/client/services/PersonalContainer';
 import PersonalContainer from '~/client/services/PersonalContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { apiv3Put } from '~/client/util/apiv3-client';
 
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
 
 
@@ -23,7 +24,7 @@ class ApiSettings extends React.Component {
     const { t, appContainer, personalContainer } = this.props;
     const { t, appContainer, personalContainer } = this.props;
 
 
     try {
     try {
-      await appContainer.apiv3Put('/personal-setting/api-token');
+      await apiv3Put('/personal-setting/api-token');
 
 
       await personalContainer.retrievePersonalData();
       await personalContainer.retrievePersonalData();
       toastSuccess(t('toaster.update_successed', { target: t('page_me_apitoken.api_token') }));
       toastSuccess(t('toaster.update_successed', { target: t('page_me_apitoken.api_token') }));

+ 6 - 25
packages/app/src/components/Me/EditorSettings.tsx

@@ -3,17 +3,13 @@ import React, {
   FC, SetStateAction, useCallback, useEffect, useState,
   FC, SetStateAction, useCallback, useEffect, useState,
 } from 'react';
 } from 'react';
 
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
-
-import { withUnstatedContainers } from '../UnstatedUtils';
+import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
 
 
 
 
 type EditorSettingsBodyProps = {
 type EditorSettingsBodyProps = {
-  appContainer: AppContainer
 }
 }
 
 
 type RuleListGroupProps = {
 type RuleListGroupProps = {
@@ -153,7 +149,7 @@ const japaneseRulesMenuItems = [
 
 
 const RuleListGroup: FC<RuleListGroupProps> = ({
 const RuleListGroup: FC<RuleListGroupProps> = ({
   title, ruleList, textlintRules, setTextlintRules,
   title, ruleList, textlintRules, setTextlintRules,
-}) => {
+}: RuleListGroupProps) => {
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
   const isCheckedRule = (ruleName: string) => (
   const isCheckedRule = (ruleName: string) => (
@@ -200,21 +196,12 @@ const RuleListGroup: FC<RuleListGroupProps> = ({
 };
 };
 
 
 
 
-RuleListGroup.propTypes = {
-  title: PropTypes.string.isRequired,
-  ruleList: PropTypes.array.isRequired,
-  textlintRules: PropTypes.array.isRequired,
-  setTextlintRules: PropTypes.func.isRequired,
-};
-
-
-const EditorSettingsBody: FC<EditorSettingsBodyProps> = (props) => {
+export const EditorSettings: FC<EditorSettingsBodyProps> = () => {
   const { t } = useTranslation();
   const { t } = useTranslation();
-  const { appContainer } = props;
   const [textlintRules, setTextlintRules] = useState<LintRule[]>([]);
   const [textlintRules, setTextlintRules] = useState<LintRule[]>([]);
 
 
   const initializeEditorSettings = useCallback(async() => {
   const initializeEditorSettings = useCallback(async() => {
-    const { data } = await appContainer.apiv3Get('/personal-setting/editor-settings');
+    const { data } = await apiv3Get('/personal-setting/editor-settings');
     const retrievedRules: LintRule[] = data?.textlintSettings?.textlintRules;
     const retrievedRules: LintRule[] = data?.textlintSettings?.textlintRules;
 
 
     // If database is empty, add default rules to state
     // If database is empty, add default rules to state
@@ -234,7 +221,7 @@ const EditorSettingsBody: FC<EditorSettingsBodyProps> = (props) => {
       setTextlintRules([...defaultCommonRules, ...defaultJapaneseRules]);
       setTextlintRules([...defaultCommonRules, ...defaultJapaneseRules]);
     }
     }
 
 
-  }, [appContainer]);
+  }, []);
 
 
   useEffect(() => {
   useEffect(() => {
     initializeEditorSettings();
     initializeEditorSettings();
@@ -242,7 +229,7 @@ const EditorSettingsBody: FC<EditorSettingsBodyProps> = (props) => {
 
 
   const updateRulesHandler = async() => {
   const updateRulesHandler = async() => {
     try {
     try {
-      const { data } = await appContainer.apiv3Put('/personal-setting/editor-settings', { textlintSettings: { textlintRules: [...textlintRules] } });
+      const { data } = await apiv3Put('/personal-setting/editor-settings', { textlintSettings: { textlintRules: [...textlintRules] } });
       setTextlintRules(data.textlintSettings.textlintRules);
       setTextlintRules(data.textlintSettings.textlintRules);
       toastSuccess(t('toaster.update_successed', { target: 'Updated Textlint Settings' }));
       toastSuccess(t('toaster.update_successed', { target: 'Updated Textlint Settings' }));
     }
     }
@@ -285,9 +272,3 @@ const EditorSettingsBody: FC<EditorSettingsBodyProps> = (props) => {
     </div>
     </div>
   );
   );
 };
 };
-
-export const EditorSettings = withUnstatedContainers(EditorSettingsBody, [AppContainer]);
-
-EditorSettingsBody.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-};

+ 6 - 11
packages/app/src/components/Me/PasswordSettings.jsx

@@ -4,20 +4,18 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
-import AppContainer from '~/client/services/AppContainer';
 import PersonalContainer from '~/client/services/PersonalContainer';
 import PersonalContainer from '~/client/services/PersonalContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
 
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
 
 
 
 
 class PasswordSettings extends React.Component {
 class PasswordSettings extends React.Component {
 
 
-  constructor(appContainer) {
+  constructor() {
     super();
     super();
 
 
-    this.appContainer = appContainer;
-
     this.state = {
     this.state = {
       retrieveError: null,
       retrieveError: null,
       oldPassword: '',
       oldPassword: '',
@@ -32,10 +30,8 @@ class PasswordSettings extends React.Component {
   }
   }
 
 
   async componentDidMount() {
   async componentDidMount() {
-    const { appContainer } = this.props;
-
     try {
     try {
-      const res = await appContainer.apiv3Get('/personal-setting/is-password-set');
+      const res = await apiv3Get('/personal-setting/is-password-set');
       const { isPasswordSet } = res.data;
       const { isPasswordSet } = res.data;
       this.setState({ isPasswordSet });
       this.setState({ isPasswordSet });
     }
     }
@@ -47,11 +43,11 @@ class PasswordSettings extends React.Component {
   }
   }
 
 
   async onClickSubmit() {
   async onClickSubmit() {
-    const { t, appContainer, personalContainer } = this.props;
+    const { t, personalContainer } = this.props;
     const { oldPassword, newPassword, newPasswordConfirm } = this.state;
     const { oldPassword, newPassword, newPasswordConfirm } = this.state;
 
 
     try {
     try {
-      await appContainer.apiv3Put('/personal-setting/password', {
+      await apiv3Put('/personal-setting/password', {
         oldPassword, newPassword, newPasswordConfirm,
         oldPassword, newPassword, newPasswordConfirm,
       });
       });
       this.setState({ oldPassword: '', newPassword: '', newPasswordConfirm: '' });
       this.setState({ oldPassword: '', newPassword: '', newPasswordConfirm: '' });
@@ -159,11 +155,10 @@ class PasswordSettings extends React.Component {
 }
 }
 
 
 
 
-const PasswordSettingsWrapper = withUnstatedContainers(PasswordSettings, [AppContainer, PersonalContainer]);
+const PasswordSettingsWrapper = withUnstatedContainers(PasswordSettings, [PersonalContainer]);
 
 
 PasswordSettings.propTypes = {
 PasswordSettings.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   personalContainer: PropTypes.instanceOf(PersonalContainer).isRequired,
   personalContainer: PropTypes.instanceOf(PersonalContainer).isRequired,
 };
 };
 
 

+ 6 - 7
packages/app/src/components/MyDraftList/MyDraftList.jsx

@@ -1,14 +1,14 @@
 import React from 'react';
 import React from 'react';
-import PropTypes from 'prop-types';
 
 
+import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
-import { withUnstatedContainers } from '../UnstatedUtils';
-import AppContainer from '~/client/services/AppContainer';
-import PageContainer from '~/client/services/PageContainer';
 import EditorContainer from '~/client/services/EditorContainer';
 import EditorContainer from '~/client/services/EditorContainer';
+import PageContainer from '~/client/services/PageContainer';
+import { apiGet } from '~/client/util/apiv1-client';
 
 
 import PaginationWrapper from '../PaginationWrapper';
 import PaginationWrapper from '../PaginationWrapper';
+import { withUnstatedContainers } from '../UnstatedUtils';
 
 
 import Draft from './Draft';
 import Draft from './Draft';
 
 
@@ -49,7 +49,7 @@ class MyDraftList extends React.Component {
       return;
       return;
     }
     }
 
 
-    const res = await this.props.appContainer.apiGet('/pages.exist', {
+    const res = await apiGet('/pages.exist', {
       pagePaths: JSON.stringify(Object.keys(draftsAsObj)),
       pagePaths: JSON.stringify(Object.keys(draftsAsObj)),
     });
     });
 
 
@@ -174,13 +174,12 @@ class MyDraftList extends React.Component {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const MyDraftListWrapper = withUnstatedContainers(MyDraftList, [AppContainer, PageContainer, EditorContainer]);
+const MyDraftListWrapper = withUnstatedContainers(MyDraftList, [PageContainer, EditorContainer]);
 
 
 
 
 MyDraftList.propTypes = {
 MyDraftList.propTypes = {
   t: PropTypes.func.isRequired, // react-i18next
   t: PropTypes.func.isRequired, // react-i18next
 
 
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
   editorContainer: PropTypes.instanceOf(EditorContainer).isRequired,
   editorContainer: PropTypes.instanceOf(EditorContainer).isRequired,
 };
 };

+ 174 - 0
packages/app/src/components/Navbar/AppearanceModeDropdown.tsx

@@ -0,0 +1,174 @@
+import React, { FC, useState, useCallback } from 'react';
+
+import { useTranslation } from 'react-i18next';
+import { UncontrolledTooltip } from 'reactstrap';
+
+import { useUserUISettings } from '~/client/services/user-ui-settings';
+import {
+  isUserPreferenceExists,
+  isDarkMode as isDarkModeByUtil,
+  applyColorScheme,
+  removeUserPreference,
+  updateUserPreference,
+  updateUserPreferenceWithOsSettings,
+} from '~/client/util/color-scheme';
+import { usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser } from '~/stores/ui';
+
+import MoonIcon from '../Icons/MoonIcon';
+import SidebarDockIcon from '../Icons/SidebarDockIcon';
+import SidebarDrawerIcon from '../Icons/SidebarDrawerIcon';
+import SunIcon from '../Icons/SunIcon';
+
+type AppearanceModeDropdownProps = {
+  isAuthenticated: boolean,
+}
+export const AppearanceModeDropdown:FC<AppearanceModeDropdownProps> = (props: AppearanceModeDropdownProps) => {
+
+  const { t } = useTranslation();
+
+  const { isAuthenticated } = props;
+
+  const [useOsSettings, setOsSettings] = useState(!isUserPreferenceExists());
+  const [isDarkMode, setIsDarkMode] = useState(isDarkModeByUtil());
+
+  const { data: isPreferDrawerMode, update: updatePreferDrawerMode } = usePreferDrawerModeByUser();
+  const { data: isPreferDrawerModeOnEdit, mutate: mutatePreferDrawerModeOnEdit } = usePreferDrawerModeOnEditByUser();
+  const { scheduleToPut } = useUserUISettings();
+
+  const preferDrawerModeSwitchModifiedHandler = useCallback((preferDrawerMode: boolean, isEditMode: boolean) => {
+    if (isEditMode) {
+      mutatePreferDrawerModeOnEdit(preferDrawerMode);
+      scheduleToPut({ preferDrawerModeOnEditByUser: preferDrawerMode });
+    }
+    else {
+      updatePreferDrawerMode(preferDrawerMode);
+    }
+  }, [updatePreferDrawerMode, mutatePreferDrawerModeOnEdit, scheduleToPut]);
+
+  const followOsCheckboxModifiedHandler = useCallback((useOsSettings: boolean) => {
+    if (useOsSettings) {
+      removeUserPreference();
+    }
+    else {
+      updateUserPreferenceWithOsSettings();
+    }
+    applyColorScheme();
+
+    // update states
+    setOsSettings(useOsSettings);
+    setIsDarkMode(isDarkModeByUtil());
+  }, []);
+
+  const userPreferenceSwitchModifiedHandler = useCallback((isDarkMode: boolean) => {
+    updateUserPreference(isDarkMode);
+    applyColorScheme();
+
+    // update state
+    setIsDarkMode(isDarkModeByUtil());
+  }, []);
+
+  /* eslint-disable react/prop-types */
+  const IconWithTooltip = ({
+    id, label, children, additionalClasses,
+  }) => (
+    <>
+      <div id={id} className={`px-2 grw-icon-container ${additionalClasses != null ? additionalClasses : ''}`}>{children}</div>
+      <UncontrolledTooltip placement="bottom" fade={false} target={id}>{label}</UncontrolledTooltip>
+    </>
+  );
+
+  const renderSidebarModeSwitch = useCallback((isEditMode: boolean) => {
+    return (
+      <>
+        <h6 className="dropdown-header">{t(isEditMode ? 'personal_dropdown.sidebar_mode_editor' : 'personal_dropdown.sidebar_mode')}</h6>
+        <form className="px-4">
+          <div className="form-row justify-content-center">
+            <div className="form-group col-auto mb-0 d-flex align-items-center">
+              <IconWithTooltip id={isEditMode ? 'iwt-sidebar-editor-drawer' : 'iwt-sidebar-drawer'} label="Drawer" additionalClasses="grw-sidebar-mode-icon">
+                <SidebarDrawerIcon />
+              </IconWithTooltip>
+              <div className="custom-control custom-switch custom-checkbox-secondary ml-2">
+                <input
+                  id={isEditMode ? 'swSidebarModeOnEditor' : 'swSidebarMode'}
+                  className="custom-control-input"
+                  type="checkbox"
+                  checked={isEditMode ? !isPreferDrawerModeOnEdit : !isPreferDrawerMode}
+                  onChange={e => preferDrawerModeSwitchModifiedHandler(!e.target.checked, isEditMode)}
+                />
+                <label className="custom-control-label" htmlFor={isEditMode ? 'swSidebarModeOnEditor' : 'swSidebarMode'}></label>
+              </div>
+              <IconWithTooltip id={isEditMode ? 'iwt-sidebar-editor-dock' : 'iwt-sidebar-dock'} label="Dock" additionalClasses="grw-sidebar-mode-icon">
+                <SidebarDockIcon />
+              </IconWithTooltip>
+            </div>
+          </div>
+        </form>
+      </>
+    );
+  }, [isPreferDrawerMode, isPreferDrawerModeOnEdit, preferDrawerModeSwitchModifiedHandler, t]);
+
+  return (
+    <>
+      {/* setting button */}
+      <button className="bg-transparent border-0 nav-link" type="button" data-toggle="dropdown" aria-haspopup="true">
+        <i className="icon-settings"></i>
+      </button>
+
+      {/* dropdown */}
+      <div className="dropdown-menu dropdown-menu-right">
+
+        {/* sidebar mode */}
+        {renderSidebarModeSwitch(false)}
+
+        <div className="dropdown-divider"></div>
+
+        {/* side bar mode on editor */}
+        {isAuthenticated && renderSidebarModeSwitch(true)}
+
+        <div className="dropdown-divider"></div>
+
+        {/* color mode */}
+        <h6 className="dropdown-header">{t('personal_dropdown.color_mode')}</h6>
+        <form className="px-4">
+          <div className="form-row justify-content-center">
+            <div className="form-group col-auto d-flex align-items-center">
+              <IconWithTooltip id="iwt-light" label="Light" additionalClasses={useOsSettings ? 'grw-color-mode-icon-muted' : 'grw-color-mode-icon'}>
+                <SunIcon />
+              </IconWithTooltip>
+              <div className="custom-control custom-switch custom-checkbox-secondary ml-2">
+                <input
+                  id="swUserPreference"
+                  className="custom-control-input"
+                  type="checkbox"
+                  checked={isDarkMode}
+                  disabled={useOsSettings}
+                  onChange={e => userPreferenceSwitchModifiedHandler(e.target.checked)}
+                />
+                <label className="custom-control-label" htmlFor="swUserPreference"></label>
+              </div>
+              <IconWithTooltip id="iwt-dark" label="Dark" additionalClasses={useOsSettings ? 'grw-color-mode-icon-muted' : 'grw-color-mode-icon'}>
+                <MoonIcon />
+              </IconWithTooltip>
+            </div>
+          </div>
+          <div className="form-row">
+            <div className="form-group col-auto">
+              <div className="custom-control custom-checkbox">
+                <input
+                  id="cbFollowOs"
+                  className="custom-control-input"
+                  type="checkbox"
+                  checked={useOsSettings}
+                  onChange={e => followOsCheckboxModifiedHandler(e.target.checked)}
+                />
+                <label className="custom-control-label text-nowrap" htmlFor="cbFollowOs">{t('personal_dropdown.use_os_settings')}</label>
+              </div>
+            </div>
+          </div>
+        </form>
+      </div>
+
+    </>
+  );
+
+};

+ 46 - 25
packages/app/src/components/Navbar/GrowiNavbar.tsx

@@ -1,4 +1,4 @@
-import React, { FC, memo } from 'react';
+import React, { FC, memo, useMemo } from 'react';
 
 
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
@@ -14,6 +14,7 @@ import GrowiLogo from '../Icons/GrowiLogo';
 import InAppNotificationDropdown from '../InAppNotification/InAppNotificationDropdown';
 import InAppNotificationDropdown from '../InAppNotification/InAppNotificationDropdown';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
 
 
+import { AppearanceModeDropdown } from './AppearanceModeDropdown';
 import GlobalSearch from './GlobalSearch';
 import GlobalSearch from './GlobalSearch';
 import PersonalDropdown from './PersonalDropdown';
 import PersonalDropdown from './PersonalDropdown';
 
 
@@ -27,33 +28,53 @@ const NavbarRight: FC<NavbarRightProps> = memo((props: NavbarRightProps) => {
   const { open: openCreateModal } = usePageCreateModal();
   const { open: openCreateModal } = usePageCreateModal();
 
 
   const { currentUser } = props;
   const { currentUser } = props;
-
-  // render login button
-  if (currentUser == null) {
-    return <li id="login-user" className="nav-item"><a className="nav-link" href="/login">Login</a></li>;
-  }
+  const isAuthenticated = currentUser != null;
+
+  const authenticatedNavItem = useMemo(() => {
+    return (
+      <>
+        <li className="nav-item">
+          <InAppNotificationDropdown />
+        </li>
+
+        <li className="nav-item d-none d-md-block">
+          <button
+            className="px-md-3 nav-link btn-create-page border-0 bg-transparent"
+            type="button"
+            data-testid="newPageBtn"
+            onClick={() => openCreateModal(currentPagePath || '')}
+          >
+            <i className="icon-pencil mr-2"></i>
+            <span className="d-none d-lg-block">{ t('New') }</span>
+          </button>
+        </li>
+
+        <li className="grw-personal-dropdown nav-item dropdown">
+          <AppearanceModeDropdown isAuthenticated={isAuthenticated} />
+        </li>
+
+        <li className="grw-personal-dropdown nav-item dropdown dropdown-toggle dropdown-toggle-no-caret" data-testid="grw-personal-dropdown">
+          <PersonalDropdown />
+        </li>
+      </>
+    );
+  }, [t, currentPagePath, openCreateModal, isAuthenticated]);
+
+  const notAuthenticatedNavItem = useMemo(() => {
+    return (
+      <>
+        <li className="grw-personal-dropdown nav-item dropdown">
+          <AppearanceModeDropdown isAuthenticated={isAuthenticated} />
+        </li>
+
+        <li id="login-user" className="nav-item"><a className="nav-link" href="/login">Login</a></li>;
+      </>
+    );
+  }, []);
 
 
   return (
   return (
     <>
     <>
-      <li className="nav-item">
-        <InAppNotificationDropdown />
-      </li>
-
-      <li className="nav-item d-none d-md-block">
-        <button
-          className="px-md-3 nav-link btn-create-page border-0 bg-transparent"
-          type="button"
-          data-testid="newPageBtn"
-          onClick={() => openCreateModal(currentPagePath || '')}
-        >
-          <i className="icon-pencil mr-2"></i>
-          <span className="d-none d-lg-block">{ t('New') }</span>
-        </button>
-      </li>
-
-      <li className="grw-personal-dropdown nav-item dropdown dropdown-toggle dropdown-toggle-no-caret" data-testid="grw-personal-dropdown">
-        <PersonalDropdown />
-      </li>
+      {isAuthenticated ? authenticatedNavItem : notAuthenticatedNavItem}
     </>
     </>
   );
   );
 });
 });

+ 3 - 1
packages/app/src/components/Navbar/PageEditorModeManager.jsx

@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
 import { UncontrolledTooltip } from 'reactstrap';
 import { UncontrolledTooltip } from 'reactstrap';
 
 
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
+import { useCurrentUser } from '~/stores/context';
 import { EditorMode, useIsDeviceSmallerThanMd } from '~/stores/ui';
 import { EditorMode, useIsDeviceSmallerThanMd } from '~/stores/ui';
 
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
@@ -45,8 +46,9 @@ function PageEditorModeManager(props) {
 
 
   const { t } = useTranslation();
   const { t } = useTranslation();
   const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
   const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
+  const { data: currentUser } = useCurrentUser();
 
 
-  const isAdmin = appContainer.isAdmin;
+  const isAdmin = currentUser?.isAdmin;
   const isHackmdEnabled = appContainer.config.env.HACKMD_URI != null;
   const isHackmdEnabled = appContainer.config.env.HACKMD_URI != null;
   const showHackmdBtn = isHackmdEnabled || isAdmin;
   const showHackmdBtn = isHackmdEnabled || isAdmin;
 
 

+ 1 - 164
packages/app/src/components/Navbar/PersonalDropdown.jsx

@@ -1,29 +1,11 @@
-import React, { useState, useCallback } from 'react';
+import React from 'react';
 
 
 import { UserPicture } from '@growi/ui';
 import { UserPicture } from '@growi/ui';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
-import { UncontrolledTooltip } from 'reactstrap';
 
 
-import { useUserUISettings } from '~/client/services/user-ui-settings';
 import { toastError } from '~/client/util/apiNotification';
 import { toastError } from '~/client/util/apiNotification';
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { apiv3Post } from '~/client/util/apiv3-client';
-import {
-  isUserPreferenceExists,
-  isDarkMode as isDarkModeByUtil,
-  applyColorScheme,
-  removeUserPreference,
-  updateUserPreference,
-  updateUserPreferenceWithOsSettings,
-} from '~/client/util/color-scheme';
 import { useCurrentUser } from '~/stores/context';
 import { useCurrentUser } from '~/stores/context';
-import { usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser } from '~/stores/ui';
-
-
-import MoonIcon from '../Icons/MoonIcon';
-import SidebarDockIcon from '../Icons/SidebarDockIcon';
-import SidebarDrawerIcon from '../Icons/SidebarDrawerIcon';
-import SunIcon from '../Icons/SunIcon';
-
 
 
 const PersonalDropdown = () => {
 const PersonalDropdown = () => {
   const { t } = useTranslation();
   const { t } = useTranslation();
@@ -31,13 +13,6 @@ const PersonalDropdown = () => {
 
 
   const user = currentUser || {};
   const user = currentUser || {};
 
 
-  const [useOsSettings, setOsSettings] = useState(!isUserPreferenceExists());
-  const [isDarkMode, setIsDarkMode] = useState(isDarkModeByUtil());
-
-  const { data: isPreferDrawerMode, mutate: mutatePreferDrawerMode } = usePreferDrawerModeByUser();
-  const { data: isPreferDrawerModeOnEdit, mutate: mutatePreferDrawerModeOnEdit } = usePreferDrawerModeOnEditByUser();
-  const { scheduleToPut } = useUserUISettings();
-
   const logoutHandler = async() => {
   const logoutHandler = async() => {
     try {
     try {
       await apiv3Post('/logout');
       await apiv3Post('/logout');
@@ -48,50 +23,6 @@ const PersonalDropdown = () => {
     }
     }
   };
   };
 
 
-  const preferDrawerModeSwitchModifiedHandler = useCallback((bool) => {
-    mutatePreferDrawerMode(bool);
-    scheduleToPut({ preferDrawerModeByUser: bool });
-  }, [mutatePreferDrawerMode, scheduleToPut]);
-
-  const preferDrawerModeOnEditSwitchModifiedHandler = useCallback((bool) => {
-    mutatePreferDrawerModeOnEdit(bool);
-    scheduleToPut({ preferDrawerModeOnEditByUser: bool });
-  }, [mutatePreferDrawerModeOnEdit, scheduleToPut]);
-
-  const followOsCheckboxModifiedHandler = (bool) => {
-    if (bool) {
-      removeUserPreference();
-    }
-    else {
-      updateUserPreferenceWithOsSettings();
-    }
-    applyColorScheme();
-
-    // update states
-    setOsSettings(bool);
-    setIsDarkMode(isDarkModeByUtil());
-  };
-
-  const userPreferenceSwitchModifiedHandler = (bool) => {
-    updateUserPreference(bool);
-    applyColorScheme();
-
-    // update state
-    setIsDarkMode(isDarkModeByUtil());
-  };
-
-
-  /* eslint-disable react/prop-types */
-  const IconWithTooltip = ({
-    id, label, children, additionalClasses,
-  }) => (
-    <>
-      <div id={id} className={`px-2 grw-icon-container ${additionalClasses != null ? additionalClasses : ''}`}>{children}</div>
-      <UncontrolledTooltip placement="bottom" fade={false} target={id}>{label}</UncontrolledTooltip>
-    </>
-  );
-  /* eslint-enable react/prop-types */
-
   return (
   return (
     <>
     <>
       {/* Button */}
       {/* Button */}
@@ -128,100 +59,6 @@ const PersonalDropdown = () => {
 
 
         <div className="dropdown-divider"></div>
         <div className="dropdown-divider"></div>
 
 
-        {/* Sidebar Mode */}
-        <h6 className="dropdown-header">{t('personal_dropdown.sidebar_mode')}</h6>
-        <form className="px-4">
-          <div className="form-row justify-content-center">
-            <div className="form-group col-auto mb-0 d-flex align-items-center">
-              <IconWithTooltip id="iwt-sidebar-drawer" label="Drawer">
-                <SidebarDrawerIcon />
-              </IconWithTooltip>
-              <div className="custom-control custom-switch custom-checkbox-secondary ml-2">
-                <input
-                  id="swSidebarMode"
-                  className="custom-control-input"
-                  type="checkbox"
-                  checked={!isPreferDrawerMode}
-                  onChange={e => preferDrawerModeSwitchModifiedHandler(!e.target.checked)}
-                />
-                <label className="custom-control-label" htmlFor="swSidebarMode"></label>
-              </div>
-              <IconWithTooltip id="iwt-sidebar-dock" label="Dock">
-                <SidebarDockIcon />
-              </IconWithTooltip>
-            </div>
-          </div>
-        </form>
-
-        {/* Sidebar Mode on Editor */}
-        <h6 className="dropdown-header">{t('personal_dropdown.sidebar_mode_editor')}</h6>
-        <form className="px-4">
-          <div className="form-row justify-content-center">
-            <div className="form-group col-auto mb-0 d-flex align-items-center">
-              <IconWithTooltip id="iwt-sidebar-editor-drawer" label="Drawer">
-                <SidebarDrawerIcon />
-              </IconWithTooltip>
-              <div className="custom-control custom-switch custom-checkbox-secondary ml-2">
-                <input
-                  id="swSidebarModeOnEditor"
-                  className="custom-control-input"
-                  type="checkbox"
-                  checked={!isPreferDrawerModeOnEdit}
-                  onChange={e => preferDrawerModeOnEditSwitchModifiedHandler(!e.target.checked)}
-                />
-                <label className="custom-control-label" htmlFor="swSidebarModeOnEditor"></label>
-              </div>
-              <IconWithTooltip id="iwt-sidebar-editor-dock" label="Dock">
-                <SidebarDockIcon />
-              </IconWithTooltip>
-            </div>
-          </div>
-        </form>
-
-        <div className="dropdown-divider"></div>
-
-        {/* Color Mode */}
-        <h6 className="dropdown-header">{t('personal_dropdown.color_mode')}</h6>
-        <form className="px-4">
-          <div className="form-row">
-            <div className="form-group col-auto">
-              <div className="custom-control custom-checkbox">
-                <input
-                  id="cbFollowOs"
-                  className="custom-control-input"
-                  type="checkbox"
-                  checked={useOsSettings}
-                  onChange={e => followOsCheckboxModifiedHandler(e.target.checked)}
-                />
-                <label className="custom-control-label text-nowrap" htmlFor="cbFollowOs">{t('personal_dropdown.use_os_settings')}</label>
-              </div>
-            </div>
-          </div>
-          <div className="form-row justify-content-center">
-            <div className="form-group col-auto mb-0 d-flex align-items-center">
-              <IconWithTooltip id="iwt-light" label="Light" additionalClasses={useOsSettings ? 'grw-icon-container-muted' : ''}>
-                <SunIcon />
-              </IconWithTooltip>
-              <div className="custom-control custom-switch custom-checkbox-secondary ml-2">
-                <input
-                  id="swUserPreference"
-                  className="custom-control-input"
-                  type="checkbox"
-                  checked={isDarkMode}
-                  disabled={useOsSettings}
-                  onChange={e => userPreferenceSwitchModifiedHandler(e.target.checked)}
-                />
-                <label className="custom-control-label" htmlFor="swUserPreference"></label>
-              </div>
-              <IconWithTooltip id="iwt-dark" label="Dark" additionalClasses={useOsSettings ? 'grw-icon-container-muted' : ''}>
-                <MoonIcon />
-              </IconWithTooltip>
-            </div>
-          </div>
-        </form>
-
-        <div className="dropdown-divider"></div>
-
         <button type="button" className="dropdown-item" onClick={logoutHandler}><i className="icon-fw icon-power"></i>{ t('Sign out') }</button>
         <button type="button" className="dropdown-item" onClick={logoutHandler}><i className="icon-fw icon-power"></i>{ t('Sign out') }</button>
       </div>
       </div>
 
 

+ 28 - 22
packages/app/src/components/Page.jsx

@@ -1,30 +1,30 @@
 import React from 'react';
 import React from 'react';
+
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
-import loggerFactory from '~/utils/logger';
 
 
-import { withUnstatedContainers } from './UnstatedUtils';
-import AppContainer from '~/client/services/AppContainer';
-import PageContainer from '~/client/services/PageContainer';
-import EditorContainer from '~/client/services/EditorContainer';
 
 
 import MarkdownTable from '~/client/models/MarkdownTable';
 import MarkdownTable from '~/client/models/MarkdownTable';
+import AppContainer from '~/client/services/AppContainer';
+import EditorContainer from '~/client/services/EditorContainer';
+import PageContainer from '~/client/services/PageContainer';
+import { getOptionsToSave } from '~/client/util/editor';
+import {
+  useCurrentPagePath, useIsGuestUser, useSlackChannels,
+} from '~/stores/context';
+import { useIsSlackEnabled } from '~/stores/editor';
+import {
+  useEditorMode, useIsMobile, useSelectedGrant, useSelectedGrantGroupId, useSelectedGrantGroupName,
+} from '~/stores/ui';
+import loggerFactory from '~/utils/logger';
 
 
-import LinkEditModal from './PageEditor/LinkEditModal';
 import RevisionRenderer from './Page/RevisionRenderer';
 import RevisionRenderer from './Page/RevisionRenderer';
+import DrawioModal from './PageEditor/DrawioModal';
 import GridEditModal from './PageEditor/GridEditModal';
 import GridEditModal from './PageEditor/GridEditModal';
 import HandsontableModal from './PageEditor/HandsontableModal';
 import HandsontableModal from './PageEditor/HandsontableModal';
-import DrawioModal from './PageEditor/DrawioModal';
-import mtu from './PageEditor/MarkdownTableUtil';
+import LinkEditModal from './PageEditor/LinkEditModal';
 import mdu from './PageEditor/MarkdownDrawioUtil';
 import mdu from './PageEditor/MarkdownDrawioUtil';
-
-import { getOptionsToSave } from '~/client/util/editor';
-
-// TODO: remove this when omitting unstated is completed
-import {
-  useEditorMode, useSelectedGrant, useSelectedGrantGroupId, useSelectedGrantGroupName,
-} from '~/stores/ui';
-import { useIsSlackEnabled } from '~/stores/editor';
-import { useCurrentPagePath, useSlackChannels } from '~/stores/context';
+import mtu from './PageEditor/MarkdownTableUtil';
+import { withUnstatedContainers } from './UnstatedUtils';
 
 
 const logger = loggerFactory('growi:Page');
 const logger = loggerFactory('growi:Page');
 
 
@@ -143,9 +143,9 @@ class Page extends React.Component {
   }
   }
 
 
   render() {
   render() {
-    const { appContainer, pageContainer, pagePath } = this.props;
-    const { isMobile } = appContainer;
-    const isLoggedIn = appContainer.currentUser != null;
+    const {
+      pageContainer, pagePath, isMobile, isGuestUser,
+    } = this.props;
     const { markdown, revisionId } = pageContainer.state;
     const { markdown, revisionId } = pageContainer.state;
 
 
     return (
     return (
@@ -155,7 +155,7 @@ class Page extends React.Component {
           <RevisionRenderer growiRenderer={this.growiRenderer} markdown={markdown} pagePath={pagePath} />
           <RevisionRenderer growiRenderer={this.growiRenderer} markdown={markdown} pagePath={pagePath} />
         )}
         )}
 
 
-        { isLoggedIn && (
+        { !isGuestUser && (
           <>
           <>
             <GridEditModal ref={this.gridEditModal} />
             <GridEditModal ref={this.gridEditModal} />
             <LinkEditModal ref={this.LinkEditModal} />
             <LinkEditModal ref={this.LinkEditModal} />
@@ -177,6 +177,8 @@ Page.propTypes = {
 
 
   pagePath: PropTypes.string.isRequired,
   pagePath: PropTypes.string.isRequired,
   editorMode: PropTypes.string.isRequired,
   editorMode: PropTypes.string.isRequired,
+  isGuestUser: PropTypes.bool.isRequired,
+  isMobile: PropTypes.bool,
   isSlackEnabled: PropTypes.bool.isRequired,
   isSlackEnabled: PropTypes.bool.isRequired,
   slackChannels: PropTypes.string.isRequired,
   slackChannels: PropTypes.string.isRequired,
   grant: PropTypes.number.isRequired,
   grant: PropTypes.number.isRequired,
@@ -187,13 +189,15 @@ Page.propTypes = {
 const PageWrapper = (props) => {
 const PageWrapper = (props) => {
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: editorMode } = useEditorMode();
   const { data: editorMode } = useEditorMode();
+  const { data: isGuestUser } = useIsGuestUser();
+  const { data: isMobile } = useIsMobile();
   const { data: isSlackEnabled } = useIsSlackEnabled();
   const { data: isSlackEnabled } = useIsSlackEnabled();
   const { data: slackChannels } = useSlackChannels();
   const { data: slackChannels } = useSlackChannels();
   const { data: grant } = useSelectedGrant();
   const { data: grant } = useSelectedGrant();
   const { data: grantGroupId } = useSelectedGrantGroupId();
   const { data: grantGroupId } = useSelectedGrantGroupId();
   const { data: grantGroupName } = useSelectedGrantGroupName();
   const { data: grantGroupName } = useSelectedGrantGroupName();
 
 
-  if (currentPagePath == null || editorMode == null) {
+  if (currentPagePath == null || editorMode == null || isGuestUser == null) {
     return null;
     return null;
   }
   }
 
 
@@ -202,6 +206,8 @@ const PageWrapper = (props) => {
       {...props}
       {...props}
       pagePath={currentPagePath}
       pagePath={currentPagePath}
       editorMode={editorMode}
       editorMode={editorMode}
+      isGuestUser={isGuestUser}
+      isMobile={isMobile}
       isSlackEnabled={isSlackEnabled}
       isSlackEnabled={isSlackEnabled}
       slackChannels={slackChannels}
       slackChannels={slackChannels}
       grant={grant}
       grant={grant}

+ 7 - 4
packages/app/src/components/Page/RevisionLoader.jsx

@@ -1,16 +1,19 @@
 import React from 'react';
 import React from 'react';
-import PropTypes from 'prop-types';
 
 
+import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 import { Waypoint } from 'react-waypoint';
 import { Waypoint } from 'react-waypoint';
 
 
-import { withUnstatedContainers } from '../UnstatedUtils';
-import GrowiRenderer from '~/client/util/GrowiRenderer';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
+import GrowiRenderer from '~/client/util/GrowiRenderer';
+import { apiv3Get } from '~/client/util/apiv3-client';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
+import { withUnstatedContainers } from '../UnstatedUtils';
+
 import RevisionRenderer from './RevisionRenderer';
 import RevisionRenderer from './RevisionRenderer';
 
 
+
 /**
 /**
  * Load data from server and render RevisionBody component
  * Load data from server and render RevisionBody component
  */
  */
@@ -47,7 +50,7 @@ class LegacyRevisionLoader extends React.Component {
 
 
     // load data with REST API
     // load data with REST API
     try {
     try {
-      const res = await this.props.appContainer.apiv3Get(`/revisions/${revisionId}`, { pageId });
+      const res = await apiv3Get(`/revisions/${revisionId}`, { pageId });
 
 
       this.setState({
       this.setState({
         markdown: res.data?.revision?.body,
         markdown: res.data?.revision?.body,

+ 15 - 3
packages/app/src/components/Page/RevisionRenderer.jsx

@@ -1,13 +1,17 @@
 import React from 'react';
 import React from 'react';
+
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
-import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import GrowiRenderer from '~/client/util/GrowiRenderer';
 import GrowiRenderer from '~/client/util/GrowiRenderer';
-import { addSmoothScrollEvent } from '~/client/util/smooth-scroll';
 import { blinkElem } from '~/client/util/blink-section-header';
 import { blinkElem } from '~/client/util/blink-section-header';
+import { addSmoothScrollEvent } from '~/client/util/smooth-scroll';
+import { useEditorSettings } from '~/stores/editor';
+
+import { withUnstatedContainers } from '../UnstatedUtils';
 
 
 import RevisionBody from './RevisionBody';
 import RevisionBody from './RevisionBody';
+
 import { loggerFactory } from '^/../codemirror-textlint/src/utils/logger';
 import { loggerFactory } from '^/../codemirror-textlint/src/utils/logger';
 
 
 const logger = loggerFactory('components:Page:RevisionRenderer');
 const logger = loggerFactory('components:Page:RevisionRenderer');
@@ -29,6 +33,7 @@ class LegacyRevisionRenderer extends React.PureComponent {
     this.currentRenderingContext = {
     this.currentRenderingContext = {
       markdown: this.props.markdown,
       markdown: this.props.markdown,
       pagePath: this.props.pagePath,
       pagePath: this.props.pagePath,
+      editorSettings: this.editorSettings,
       currentPathname: decodeURIComponent(window.location.pathname),
       currentPathname: decodeURIComponent(window.location.pathname),
     };
     };
   }
   }
@@ -173,6 +178,7 @@ LegacyRevisionRenderer.propTypes = {
   pagePath: PropTypes.string.isRequired,
   pagePath: PropTypes.string.isRequired,
   highlightKeywords: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
   highlightKeywords: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
   additionalClassName: PropTypes.string,
   additionalClassName: PropTypes.string,
+  editorSettings: PropTypes.any,
 };
 };
 
 
 /**
 /**
@@ -183,7 +189,13 @@ const LegacyRevisionRendererWrapper = withUnstatedContainers(LegacyRevisionRende
 
 
 // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 const RevisionRenderer = (props) => {
 const RevisionRenderer = (props) => {
-  return <LegacyRevisionRendererWrapper {...props} />;
+  const { data: editorSettings } = useEditorSettings();
+
+  if (editorSettings == null) {
+    return <></>;
+  }
+
+  return <LegacyRevisionRendererWrapper {...props} editorSettings={editorSettings} />;
 };
 };
 
 
 RevisionRenderer.propTypes = {
 RevisionRenderer.propTypes = {

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

@@ -5,7 +5,7 @@ import { AsyncTypeahead } from 'react-bootstrap-typeahead';
 
 
 import { apiGet } from '~/client/util/apiv1-client';
 import { apiGet } from '~/client/util/apiv1-client';
 import { toastError } from '~/client/util/apiNotification';
 import { toastError } from '~/client/util/apiNotification';
-import { ITagsSearchApiv1Result } from '~/interfaces/tag';
+import { IResTagsSearchApiv1 } from '~/interfaces/tag';
 
 
 type TypeaheadInstance = {
 type TypeaheadInstance = {
   _handleMenuItemSelect: (activeItem: string, event: React.KeyboardEvent) => void,
   _handleMenuItemSelect: (activeItem: string, event: React.KeyboardEvent) => void,
@@ -36,7 +36,7 @@ const TagsInput: FC<Props> = (props: Props) => {
     setLoading(true);
     setLoading(true);
     try {
     try {
       // TODO: 91698 SWRize
       // TODO: 91698 SWRize
-      const res = await apiGet('/tags.search', { q: query }) as ITagsSearchApiv1Result;
+      const res = await apiGet('/tags.search', { q: query }) as IResTagsSearchApiv1;
       res.tags.unshift(query);
       res.tags.unshift(query);
       setResultTags(Array.from(new Set(res.tags)));
       setResultTags(Array.from(new Set(res.tags)));
     }
     }

+ 5 - 2
packages/app/src/components/PageAttachment.jsx

@@ -6,12 +6,15 @@ import { withTranslation } from 'react-i18next';
 
 
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import PageContainer from '~/client/services/PageContainer';
 import PageContainer from '~/client/services/PageContainer';
+import { apiPost } from '~/client/util/apiv1-client';
+import { apiv3Get } from '~/client/util/apiv3-client';
 
 
 import DeleteAttachmentModal from './PageAttachment/DeleteAttachmentModal';
 import DeleteAttachmentModal from './PageAttachment/DeleteAttachmentModal';
 import PageAttachmentList from './PageAttachment/PageAttachmentList';
 import PageAttachmentList from './PageAttachment/PageAttachmentList';
 import PaginationWrapper from './PaginationWrapper';
 import PaginationWrapper from './PaginationWrapper';
 import { withUnstatedContainers } from './UnstatedUtils';
 import { withUnstatedContainers } from './UnstatedUtils';
 
 
+
 class PageAttachment extends React.Component {
 class PageAttachment extends React.Component {
 
 
   constructor(props) {
   constructor(props) {
@@ -40,7 +43,7 @@ class PageAttachment extends React.Component {
 
 
     if (!pageId) { return }
     if (!pageId) { return }
 
 
-    const res = await this.props.appContainer.apiv3Get('/attachment/list', { pageId, page });
+    const res = await apiv3Get('/attachment/list', { pageId, page });
     const attachments = res.data.paginateResult.docs;
     const attachments = res.data.paginateResult.docs;
     const totalAttachments = res.data.paginateResult.totalDocs;
     const totalAttachments = res.data.paginateResult.totalDocs;
     const pagingLimit = res.data.paginateResult.limit;
     const pagingLimit = res.data.paginateResult.limit;
@@ -88,7 +91,7 @@ class PageAttachment extends React.Component {
       deleting: true,
       deleting: true,
     });
     });
 
 
-    this.props.appContainer.apiPost('/attachments.remove', { attachment_id: attachmentId })
+    apiPost('/attachments.remove', { attachment_id: attachmentId })
       .then((res) => {
       .then((res) => {
         this.setState({
         this.setState({
           attachments: this.state.attachments.filter((at) => {
           attachments: this.state.attachments.filter((at) => {

+ 9 - 7
packages/app/src/components/PageComment.tsx

@@ -4,17 +4,19 @@ import React, {
 
 
 import { Button } from 'reactstrap';
 import { Button } from 'reactstrap';
 
 
-import CommentEditor from './PageComment/CommentEditor';
-import Comment from './PageComment/Comment';
-import ReplayComments from './PageComment/ReplayComments';
-import DeleteCommentModal from './PageComment/DeleteCommentModal';
 
 
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import { toastError } from '~/client/util/apiNotification';
 import { toastError } from '~/client/util/apiNotification';
+import { apiPost } from '~/client/util/apiv1-client';
 
 
+import { ICommentHasId, ICommentHasIdList } from '../interfaces/comment';
 import { useSWRxPageComment } from '../stores/comment';
 import { useSWRxPageComment } from '../stores/comment';
 
 
-import { ICommentHasId, ICommentHasIdList } from '../interfaces/comment';
+
+import Comment from './PageComment/Comment';
+import CommentEditor from './PageComment/CommentEditor';
+import DeleteCommentModal from './PageComment/DeleteCommentModal';
+import ReplayComments from './PageComment/ReplayComments';
 
 
 type Props = {
 type Props = {
   appContainer: AppContainer,
   appContainer: AppContainer,
@@ -97,14 +99,14 @@ const PageComment:FC<Props> = memo((props:Props):JSX.Element => {
   const onDeleteComment = useCallback(async() => {
   const onDeleteComment = useCallback(async() => {
     if (commentToBeDeleted == null) return;
     if (commentToBeDeleted == null) return;
     try {
     try {
-      await appContainer.apiPost('/comments.remove', { comment_id: commentToBeDeleted._id });
+      await apiPost('/comments.remove', { comment_id: commentToBeDeleted._id });
       onDeleteCommentAfterOperation();
       onDeleteCommentAfterOperation();
     }
     }
     catch (error:unknown) {
     catch (error:unknown) {
       setErrorMessageOnDelete(error as string);
       setErrorMessageOnDelete(error as string);
       toastError(`error: ${error}`);
       toastError(`error: ${error}`);
     }
     }
-  }, [appContainer, commentToBeDeleted, onDeleteCommentAfterOperation]);
+  }, [commentToBeDeleted, onDeleteCommentAfterOperation]);
 
 
   const generateCommentInnerElement = (comment: ICommentHasId) => (
   const generateCommentInnerElement = (comment: ICommentHasId) => (
     <Comment
     <Comment

+ 32 - 17
packages/app/src/components/PageComment/CommentEditor.jsx

@@ -1,27 +1,28 @@
 import React from 'react';
 import React from 'react';
-import PropTypes from 'prop-types';
 
 
+import { UserPicture } from '@growi/ui';
+import PropTypes from 'prop-types';
 import {
 import {
   Button,
   Button,
   TabContent, TabPane,
   TabContent, TabPane,
 } from 'reactstrap';
 } from 'reactstrap';
-
 import * as toastr from 'toastr';
 import * as toastr from 'toastr';
 
 
-import { UserPicture } from '@growi/ui';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
-import PageContainer from '~/client/services/PageContainer';
 import CommentContainer from '~/client/services/CommentContainer';
 import CommentContainer from '~/client/services/CommentContainer';
 import EditorContainer from '~/client/services/EditorContainer';
 import EditorContainer from '~/client/services/EditorContainer';
+import PageContainer from '~/client/services/PageContainer';
 import GrowiRenderer from '~/client/util/GrowiRenderer';
 import GrowiRenderer from '~/client/util/GrowiRenderer';
+import { useCurrentUser } from '~/stores/context';
+import { useIsMobile } from '~/stores/ui';
 
 
-import { withUnstatedContainers } from '../UnstatedUtils';
+import { CustomNavTab } from '../CustomNavigation/CustomNav';
+import NotAvailableForGuest from '../NotAvailableForGuest';
 import Editor from '../PageEditor/Editor';
 import Editor from '../PageEditor/Editor';
 import { SlackNotification } from '../SlackNotification';
 import { SlackNotification } from '../SlackNotification';
+import { withUnstatedContainers } from '../UnstatedUtils';
 
 
 import CommentPreview from './CommentPreview';
 import CommentPreview from './CommentPreview';
-import NotAvailableForGuest from '../NotAvailableForGuest';
-import { CustomNavTab } from '../CustomNavigation/CustomNav';
 
 
 
 
 const navTabMapping = {
 const navTabMapping = {
@@ -272,11 +273,10 @@ class CommentEditor extends React.Component {
   }
   }
 
 
   renderReady() {
   renderReady() {
-    const { appContainer, commentContainer } = this.props;
+    const { appContainer, commentContainer, isMobile } = this.props;
     const { activeTab } = this.state;
     const { activeTab } = this.state;
 
 
     const commentPreview = this.state.isMarkdown ? this.getCommentHtml() : null;
     const commentPreview = this.state.isMarkdown ? this.getCommentHtml() : null;
-    const emojiStrategy = appContainer.getEmojiStrategy();
 
 
     const errorMessage = <span className="text-danger text-right mr-2">{this.state.errorMessage}</span>;
     const errorMessage = <span className="text-danger text-right mr-2">{this.state.errorMessage}</span>;
     const cancelButton = (
     const cancelButton = (
@@ -307,13 +307,13 @@ class CommentEditor extends React.Component {
                 value={this.state.comment}
                 value={this.state.comment}
                 isGfmMode={this.state.isMarkdown}
                 isGfmMode={this.state.isMarkdown}
                 lineNumbers={false}
                 lineNumbers={false}
-                isMobile={appContainer.isMobile}
+                isMobile={isMobile}
                 isUploadable={this.state.isUploadable}
                 isUploadable={this.state.isUploadable}
                 isUploadableFile={this.state.isUploadableFile}
                 isUploadableFile={this.state.isUploadableFile}
-                emojiStrategy={emojiStrategy}
                 onChange={this.updateState}
                 onChange={this.updateState}
                 onUpload={this.uploadHandler}
                 onUpload={this.uploadHandler}
                 onCtrlEnter={this.ctrlEnterHandler}
                 onCtrlEnter={this.ctrlEnterHandler}
+                isComment
               />
               />
               {/*
               {/*
                 Note: <OptionsSelector /> is not optimized for ComentEditor in terms of responsive design.
                 Note: <OptionsSelector /> is not optimized for ComentEditor in terms of responsive design.
@@ -383,14 +383,14 @@ class CommentEditor extends React.Component {
   }
   }
 
 
   render() {
   render() {
-    const { appContainer } = this.props;
+    const { currentUser } = this.props;
     const { isReadyToUse } = this.state;
     const { isReadyToUse } = this.state;
 
 
     return (
     return (
       <div className="form page-comment-form">
       <div className="form page-comment-form">
         <div className="comment-form">
         <div className="comment-form">
           <div className="comment-form-user">
           <div className="comment-form-user">
-            <UserPicture user={appContainer.currentUser} noLink noTooltip />
+            <UserPicture user={currentUser} noLink noTooltip />
           </div>
           </div>
           <div className="comment-form-main">
           <div className="comment-form-main">
             { !isReadyToUse
             { !isReadyToUse
@@ -405,6 +405,11 @@ class CommentEditor extends React.Component {
 
 
 }
 }
 
 
+/**
+ * Wrapper component for using unstated
+ */
+const CommentEditorHOCWrapper = withUnstatedContainers(CommentEditor, [AppContainer, PageContainer, EditorContainer, CommentContainer]);
+
 CommentEditor.propTypes = {
 CommentEditor.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
@@ -412,6 +417,8 @@ CommentEditor.propTypes = {
   commentContainer: PropTypes.instanceOf(CommentContainer).isRequired,
   commentContainer: PropTypes.instanceOf(CommentContainer).isRequired,
 
 
   growiRenderer: PropTypes.instanceOf(GrowiRenderer).isRequired,
   growiRenderer: PropTypes.instanceOf(GrowiRenderer).isRequired,
+  currentUser: PropTypes.instanceOf(Object),
+  isMobile: PropTypes.bool,
   isForNewComment: PropTypes.bool,
   isForNewComment: PropTypes.bool,
   replyTo: PropTypes.string,
   replyTo: PropTypes.string,
   currentCommentId: PropTypes.string,
   currentCommentId: PropTypes.string,
@@ -421,9 +428,17 @@ CommentEditor.propTypes = {
   onCommentButtonClicked: PropTypes.func,
   onCommentButtonClicked: PropTypes.func,
 };
 };
 
 
-/**
- * Wrapper component for using unstated
- */
-const CommentEditorWrapper = withUnstatedContainers(CommentEditor, [AppContainer, PageContainer, EditorContainer, CommentContainer]);
+const CommentEditorWrapper = (props) => {
+  const { data: isMobile } = useIsMobile();
+  const { data: currentUser } = useCurrentUser();
+
+  return (
+    <CommentEditorHOCWrapper
+      {...props}
+      currentUser={currentUser}
+      isMobile={isMobile}
+    />
+  );
+};
 
 
 export default CommentEditorWrapper;
 export default CommentEditorWrapper;

+ 1 - 3
packages/app/src/components/PageDeleteModal.tsx

@@ -129,7 +129,6 @@ const PageDeleteModal: FC = () => {
         if (onDeleted != null) {
         if (onDeleted != null) {
           onDeleted(data.paths, data.isRecursively, data.isCompletely);
           onDeleted(data.paths, data.isRecursively, data.isCompletely);
         }
         }
-
         closeDeleteModal();
         closeDeleteModal();
       }
       }
       catch (err) {
       catch (err) {
@@ -239,7 +238,6 @@ const PageDeleteModal: FC = () => {
         <div className="form-group grw-scrollable-modal-body pb-1">
         <div className="form-group grw-scrollable-modal-body pb-1">
           <label>{ t('modal_delete.deleting_page') }:</label><br />
           <label>{ t('modal_delete.deleting_page') }:</label><br />
           {/* Todo: change the way to show path on modal when too many pages are selected */}
           {/* Todo: change the way to show path on modal when too many pages are selected */}
-          {/* https://redmine.weseek.co.jp/issues/82787 */}
           {renderPagePathsToDelete()}
           {renderPagePathsToDelete()}
         </div>
         </div>
         { isDeletable && renderDeleteRecursivelyForm()}
         { isDeletable && renderDeleteRecursivelyForm()}
@@ -253,7 +251,7 @@ const PageDeleteModal: FC = () => {
           disabled={!isDeletable}
           disabled={!isDeletable}
           onClick={deleteButtonHandler}
           onClick={deleteButtonHandler}
         >
         >
-          <i className={`icon-${deleteIconAndKey[deleteMode].icon}`} aria-hidden="true"></i>
+          <i className={`mr-1 icon-${deleteIconAndKey[deleteMode].icon}`} aria-hidden="true"></i>
           { t(`modal_delete.delete_${deleteIconAndKey[deleteMode].translationKey}`) }
           { t(`modal_delete.delete_${deleteIconAndKey[deleteMode].translationKey}`) }
         </button>
         </button>
       </ModalFooter>
       </ModalFooter>

+ 79 - 68
packages/app/src/components/PageEditor.jsx

@@ -1,30 +1,31 @@
 import React from 'react';
 import React from 'react';
-import PropTypes from 'prop-types';
-import detectIndent from 'detect-indent';
 
 
-import { throttle, debounce } from 'throttle-debounce';
 import { envUtils } from '@growi/core';
 import { envUtils } from '@growi/core';
-import loggerFactory from '~/utils/logger';
+import detectIndent from 'detect-indent';
+import PropTypes from 'prop-types';
+import { throttle, debounce } from 'throttle-debounce';
 
 
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
+import EditorContainer from '~/client/services/EditorContainer';
 import PageContainer from '~/client/services/PageContainer';
 import PageContainer from '~/client/services/PageContainer';
+import { apiGet, apiPost } from '~/client/util/apiv1-client';
+import { getOptionsToSave } from '~/client/util/editor';
+import { useIsEditable, useIsIndentSizeForced, useSlackChannels } from '~/stores/context';
+import { useCurrentIndentSize, useIsSlackEnabled, useIsTextlintEnabled } from '~/stores/editor';
+import {
+  useEditorMode, useIsMobile, useSelectedGrant, useSelectedGrantGroupId, useSelectedGrantGroupName,
+} from '~/stores/ui';
+import loggerFactory from '~/utils/logger';
 
 
-import { withUnstatedContainers } from './UnstatedUtils';
+
+import { ConflictDiffModal } from './PageEditor/ConflictDiffModal';
 import Editor from './PageEditor/Editor';
 import Editor from './PageEditor/Editor';
 import Preview from './PageEditor/Preview';
 import Preview from './PageEditor/Preview';
 import scrollSyncHelper from './PageEditor/ScrollSyncHelper';
 import scrollSyncHelper from './PageEditor/ScrollSyncHelper';
-import { ConflictDiffModal } from './PageEditor/ConflictDiffModal';
-
-import EditorContainer from '~/client/services/EditorContainer';
+import { withUnstatedContainers } from './UnstatedUtils';
 
 
-import { getOptionsToSave } from '~/client/util/editor';
 
 
 // TODO: remove this when omitting unstated is completed
 // TODO: remove this when omitting unstated is completed
-import {
-  useEditorMode, useSelectedGrant, useSelectedGrantGroupId, useSelectedGrantGroupName,
-} from '~/stores/ui';
-import { useIsEditable, useSlackChannels } from '~/stores/context';
-import { useIsSlackEnabled } from '~/stores/editor';
 
 
 const logger = loggerFactory('growi:PageEditor');
 const logger = loggerFactory('growi:PageEditor');
 
 
@@ -74,10 +75,10 @@ class PageEditor extends React.Component {
 
 
     // Detect indent size from contents (only when users are allowed to change it)
     // Detect indent size from contents (only when users are allowed to change it)
     // TODO: https://youtrack.weseek.co.jp/issue/GW-5368
     // TODO: https://youtrack.weseek.co.jp/issue/GW-5368
-    if (!this.props.appContainer.config.isIndentSizeForced && this.state.markdown) {
+    if (!props.isIndentSizeForced && this.state.markdown) {
       const detectedIndent = detectIndent(this.state.markdown);
       const detectedIndent = detectIndent(this.state.markdown);
       if (detectedIndent.type === 'space' && new Set([2, 4]).has(detectedIndent.amount)) {
       if (detectedIndent.type === 'space' && new Set([2, 4]).has(detectedIndent.amount)) {
-        this.props.editorContainer.setState({ indentSize: detectedIndent.amount });
+        props.mutateCurrentIndentSize(detectedIndent.amount);
       }
       }
     }
     }
   }
   }
@@ -170,7 +171,7 @@ class PageEditor extends React.Component {
     } = this.props;
     } = this.props;
 
 
     try {
     try {
-      let res = await appContainer.apiGet('/attachments.limit', {
+      let res = await apiGet('/attachments.limit', {
         fileSize: file.size,
         fileSize: file.size,
       });
       });
 
 
@@ -187,7 +188,7 @@ class PageEditor extends React.Component {
         formData.append('page_id', pageContainer.state.pageId);
         formData.append('page_id', pageContainer.state.pageId);
       }
       }
 
 
-      res = await appContainer.apiPost('/attachments.add', formData);
+      res = await apiPost('/attachments.add', formData);
       const attachment = res.attachment;
       const attachment = res.attachment;
       const fileName = attachment.originalName;
       const fileName = attachment.originalName;
 
 
@@ -328,40 +329,35 @@ class PageEditor extends React.Component {
 
 
     const config = this.props.appContainer.getConfig();
     const config = this.props.appContainer.getConfig();
     const noCdn = envUtils.toBoolean(config.env.NO_CDN);
     const noCdn = envUtils.toBoolean(config.env.NO_CDN);
-    const emojiStrategy = this.props.appContainer.getEmojiStrategy();
-
-    const { path } = this.props.pageContainer.state;
 
 
     return (
     return (
-      <>
-        <div className="d-flex flex-wrap">
-          <div className="page-editor-editor-container flex-grow-1 flex-basis-0 mw-0">
-            <Editor
-              ref={(c) => { this.editor = c }}
-              value={this.state.markdown}
-              noCdn={noCdn}
-              isMobile={this.props.appContainer.isMobile}
-              isUploadable={this.state.isUploadable}
-              isUploadableFile={this.state.isUploadableFile}
-              emojiStrategy={emojiStrategy}
-              onScroll={this.onEditorScroll}
-              onScrollCursorIntoView={this.onEditorScrollCursorIntoView}
-              onChange={this.onMarkdownChanged}
-              onUpload={this.onUpload}
-              onSave={this.onSaveWithShortcut}
-            />
-          </div>
-          <div className="d-none d-lg-block page-editor-preview-container flex-grow-1 flex-basis-0 mw-0">
-            <Preview
-              markdown={this.state.markdown}
-              pagePath={path}
-              // eslint-disable-next-line no-return-assign
-              inputRef={(el) => { return this.previewElement = el }}
-              isMathJaxEnabled={this.state.isMathJaxEnabled}
-              renderMathJaxOnInit={false}
-              onScroll={this.onPreviewScroll}
-            />
-          </div>
+      <div className="d-flex flex-wrap">
+        <div className="page-editor-editor-container flex-grow-1 flex-basis-0 mw-0">
+          <Editor
+            ref={(c) => { this.editor = c }}
+            value={this.state.markdown}
+            noCdn={noCdn}
+            isMobile={this.props.isMobile}
+            isUploadable={this.state.isUploadable}
+            isUploadableFile={this.state.isUploadableFile}
+            isTextlintEnabled={this.props.isTextlintEnabled}
+            indentSize={this.props.indentSize}
+            onScroll={this.onEditorScroll}
+            onScrollCursorIntoView={this.onEditorScrollCursorIntoView}
+            onChange={this.onMarkdownChanged}
+            onUpload={this.onUpload}
+            onSave={this.onSaveWithShortcut}
+          />
+        </div>
+        <div className="d-none d-lg-block page-editor-preview-container flex-grow-1 flex-basis-0 mw-0">
+          <Preview
+            markdown={this.state.markdown}
+            // eslint-disable-next-line no-return-assign
+            inputRef={(el) => { return this.previewElement = el }}
+            isMathJaxEnabled={this.state.isMathJaxEnabled}
+            renderMathJaxOnInit={false}
+            onScroll={this.onPreviewScroll}
+          />
         </div>
         </div>
         <ConflictDiffModal
         <ConflictDiffModal
           isOpen={this.props.pageContainer.state.isConflictDiffModalOpen}
           isOpen={this.props.pageContainer.state.isConflictDiffModalOpen}
@@ -370,12 +366,34 @@ class PageEditor extends React.Component {
           pageContainer={this.props.pageContainer}
           pageContainer={this.props.pageContainer}
           markdownOnEdit={this.state.markdown}
           markdownOnEdit={this.state.markdown}
         />
         />
-      </>
+      </div>
     );
     );
   }
   }
 
 
 }
 }
 
 
+PageEditor.propTypes = {
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
+  editorContainer: PropTypes.instanceOf(EditorContainer).isRequired,
+
+  isEditable: PropTypes.bool.isRequired,
+
+  // TODO: remove this when omitting unstated is completed
+  editorMode: PropTypes.string.isRequired,
+  isMobile: PropTypes.bool,
+  isSlackEnabled: PropTypes.bool.isRequired,
+  slackChannels: PropTypes.string.isRequired,
+  grant: PropTypes.number.isRequired,
+  grantGroupId: PropTypes.string,
+  grantGroupName: PropTypes.string,
+  mutateGrant: PropTypes.func,
+  isTextlintEnabled: PropTypes.bool,
+  isIndentSizeForced: PropTypes.bool,
+  indentSize: PropTypes.number,
+  mutateCurrentIndentSize: PropTypes.func,
+};
+
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
@@ -384,11 +402,15 @@ const PageEditorHOCWrapper = withUnstatedContainers(PageEditor, [AppContainer, P
 const PageEditorWrapper = (props) => {
 const PageEditorWrapper = (props) => {
   const { data: isEditable } = useIsEditable();
   const { data: isEditable } = useIsEditable();
   const { data: editorMode } = useEditorMode();
   const { data: editorMode } = useEditorMode();
+  const { data: isMobile } = useIsMobile();
   const { data: isSlackEnabled } = useIsSlackEnabled();
   const { data: isSlackEnabled } = useIsSlackEnabled();
   const { data: slackChannels } = useSlackChannels();
   const { data: slackChannels } = useSlackChannels();
   const { data: grant, mutate: mutateGrant } = useSelectedGrant();
   const { data: grant, mutate: mutateGrant } = useSelectedGrant();
   const { data: grantGroupId } = useSelectedGrantGroupId();
   const { data: grantGroupId } = useSelectedGrantGroupId();
   const { data: grantGroupName } = useSelectedGrantGroupName();
   const { data: grantGroupName } = useSelectedGrantGroupName();
+  const { data: isTextlintEnabled } = useIsTextlintEnabled();
+  const { data: isIndentSizeForced } = useIsIndentSizeForced();
+  const { data: indentSize, mutate: mutateCurrentIndentSize } = useCurrentIndentSize();
 
 
   if (isEditable == null || editorMode == null) {
   if (isEditable == null || editorMode == null) {
     return null;
     return null;
@@ -399,31 +421,20 @@ const PageEditorWrapper = (props) => {
       {...props}
       {...props}
       isEditable={isEditable}
       isEditable={isEditable}
       editorMode={editorMode}
       editorMode={editorMode}
+      isMobile={isMobile}
       isSlackEnabled={isSlackEnabled}
       isSlackEnabled={isSlackEnabled}
       slackChannels={slackChannels}
       slackChannels={slackChannels}
       grant={grant}
       grant={grant}
       grantGroupId={grantGroupId}
       grantGroupId={grantGroupId}
       grantGroupName={grantGroupName}
       grantGroupName={grantGroupName}
       mutateGrant={mutateGrant}
       mutateGrant={mutateGrant}
+      isTextlintEnabled={isTextlintEnabled}
+      isIndentSizeForced={isIndentSizeForced}
+      indentSize={indentSize}
+      mutateCurrentIndentSize={mutateCurrentIndentSize}
+
     />
     />
   );
   );
 };
 };
 
 
-PageEditor.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
-  editorContainer: PropTypes.instanceOf(EditorContainer).isRequired,
-
-  isEditable: PropTypes.bool.isRequired,
-
-  // TODO: remove this when omitting unstated is completed
-  editorMode: PropTypes.string.isRequired,
-  isSlackEnabled: PropTypes.bool.isRequired,
-  slackChannels: PropTypes.string.isRequired,
-  grant: PropTypes.number.isRequired,
-  grantGroupId: PropTypes.string,
-  grantGroupName: PropTypes.string,
-  mutateGrant: PropTypes.func,
-};
-
 export default PageEditorWrapper;
 export default PageEditorWrapper;

+ 138 - 66
packages/app/src/components/PageEditor/CodeMirrorEditor.jsx

@@ -1,36 +1,36 @@
 import React from 'react';
 import React from 'react';
-import PropTypes from 'prop-types';
 
 
-import urljoin from 'url-join';
+import { createValidator } from '@growi/codemirror-textlint';
 import * as codemirror from 'codemirror';
 import * as codemirror from 'codemirror';
-import { Button } from 'reactstrap';
-
 import { JSHINT } from 'jshint';
 import { JSHINT } from 'jshint';
-
-import * as loadScript from 'simple-load-script';
 import * as loadCssSync from 'load-css-file';
 import * as loadCssSync from 'load-css-file';
+import PropTypes from 'prop-types';
+import { Button } from 'reactstrap';
+import * as loadScript from 'simple-load-script';
+import urljoin from 'url-join';
 
 
-import { createValidator } from '@growi/codemirror-textlint';
-import { UncontrolledCodeMirror } from '../UncontrolledCodeMirror';
 import InterceptorManager from '~/services/interceptor-manager';
 import InterceptorManager from '~/services/interceptor-manager';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-import AbstractEditor from './AbstractEditor';
-import SimpleCheatsheet from './SimpleCheatsheet';
+import { UncontrolledCodeMirror } from '../UncontrolledCodeMirror';
 
 
-import pasteHelper from './PasteHelper';
-import EmojiAutoCompleteHelper from './EmojiAutoCompleteHelper';
-import PreventMarkdownListInterceptor from './PreventMarkdownListInterceptor';
-import MarkdownTableInterceptor from './MarkdownTableInterceptor';
-import mlu from './MarkdownLinkUtil';
-import mtu from './MarkdownTableUtil';
-import mdu from './MarkdownDrawioUtil';
-import geu from './GridEditorUtil';
+import AbstractEditor from './AbstractEditor';
+import CommentMentionHelper from './CommentMentionHelper';
+import DrawioModal from './DrawioModal';
+import EditorIcon from './EditorIcon';
+import EmojiPicker from './EmojiPicker';
+import EmojiPickerHelper from './EmojiPickerHelper';
 import GridEditModal from './GridEditModal';
 import GridEditModal from './GridEditModal';
-import LinkEditModal from './LinkEditModal';
+import geu from './GridEditorUtil';
 import HandsontableModal from './HandsontableModal';
 import HandsontableModal from './HandsontableModal';
-import EditorIcon from './EditorIcon';
-import DrawioModal from './DrawioModal';
+import LinkEditModal from './LinkEditModal';
+import mdu from './MarkdownDrawioUtil';
+import mlu from './MarkdownLinkUtil';
+import MarkdownTableInterceptor from './MarkdownTableInterceptor';
+import mtu from './MarkdownTableUtil';
+import pasteHelper from './PasteHelper';
+import PreventMarkdownListInterceptor from './PreventMarkdownListInterceptor';
+import SimpleCheatsheet from './SimpleCheatsheet';
 
 
 // Textlint
 // Textlint
 window.JSHINT = JSHINT;
 window.JSHINT = JSHINT;
@@ -100,7 +100,7 @@ require('codemirror/mode/yaml/yaml');
 const MARKDOWN_TABLE_ACTIVATED_CLASS = 'markdown-table-activated';
 const MARKDOWN_TABLE_ACTIVATED_CLASS = 'markdown-table-activated';
 const MARKDOWN_LINK_ACTIVATED_CLASS = 'markdown-link-activated';
 const MARKDOWN_LINK_ACTIVATED_CLASS = 'markdown-link-activated';
 
 
-export default class CodeMirrorEditor extends AbstractEditor {
+class CodeMirrorEditor extends AbstractEditor {
 
 
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
@@ -109,11 +109,12 @@ export default class CodeMirrorEditor extends AbstractEditor {
     this.state = {
     this.state = {
       value: this.props.value,
       value: this.props.value,
       isGfmMode: this.props.isGfmMode,
       isGfmMode: this.props.isGfmMode,
-      isEnabledEmojiAutoComplete: false,
       isLoadingKeymap: false,
       isLoadingKeymap: false,
       isSimpleCheatsheetShown: this.props.isGfmMode && this.props.value.length === 0,
       isSimpleCheatsheetShown: this.props.isGfmMode && this.props.value.length === 0,
       isCheatsheetModalShown: false,
       isCheatsheetModalShown: false,
       additionalClassSet: new Set(),
       additionalClassSet: new Set(),
+      isEmojiPickerShown: false,
+      emojiSearchText: null,
     };
     };
 
 
     this.gridEditModal = React.createRef();
     this.gridEditModal = React.createRef();
@@ -138,6 +139,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
     this.pasteHandler = this.pasteHandler.bind(this);
     this.pasteHandler = this.pasteHandler.bind(this);
     this.cursorHandler = this.cursorHandler.bind(this);
     this.cursorHandler = this.cursorHandler.bind(this);
     this.changeHandler = this.changeHandler.bind(this);
     this.changeHandler = this.changeHandler.bind(this);
+    this.keyUpHandler = this.keyUpHandler.bind(this);
 
 
     this.updateCheatsheetStates = this.updateCheatsheetStates.bind(this);
     this.updateCheatsheetStates = this.updateCheatsheetStates.bind(this);
 
 
@@ -152,6 +154,8 @@ export default class CodeMirrorEditor extends AbstractEditor {
 
 
     this.foldDrawioSection = this.foldDrawioSection.bind(this);
     this.foldDrawioSection = this.foldDrawioSection.bind(this);
     this.onSaveForDrawio = this.onSaveForDrawio.bind(this);
     this.onSaveForDrawio = this.onSaveForDrawio.bind(this);
+    this.checkWhetherEmojiPickerShouldBeShown = this.checkWhetherEmojiPickerShouldBeShown.bind(this);
+
   }
   }
 
 
   init() {
   init() {
@@ -168,54 +172,61 @@ export default class CodeMirrorEditor extends AbstractEditor {
     this.loadedKeymapSet = new Set();
     this.loadedKeymapSet = new Set();
   }
   }
 
 
-  componentWillMount() {
-    if (this.props.emojiStrategy != null) {
-      this.emojiAutoCompleteHelper = new EmojiAutoCompleteHelper(this.props.emojiStrategy);
-      this.setState({ isEnabledEmojiAutoComplete: true });
-    }
-
-    this.initializeTextlint();
-  }
-
   componentDidMount() {
   componentDidMount() {
     // ensure to be able to resolve 'this' to use 'codemirror.commands.save'
     // ensure to be able to resolve 'this' to use 'codemirror.commands.save'
     this.getCodeMirror().codeMirrorEditor = this;
     this.getCodeMirror().codeMirrorEditor = this;
 
 
-    // load theme
-    const theme = this.props.editorOptions.theme;
-    this.loadTheme(theme);
-
-    // set keymap
-    const keymapMode = this.props.editorOptions.keymapMode;
-    this.setKeymapMode(keymapMode);
-
     // fold drawio section
     // fold drawio section
     this.foldDrawioSection();
     this.foldDrawioSection();
+
+    // initialize commentMentionHelper if comment editor is opened
+    if (this.props.isComment) {
+      this.commentMentionHelper = new CommentMentionHelper(this.getCodeMirror());
+    }
+    this.emojiPickerHelper = new EmojiPickerHelper(this.getCodeMirror());
+
   }
   }
 
 
   componentWillReceiveProps(nextProps) {
   componentWillReceiveProps(nextProps) {
-    // load theme
-    const theme = nextProps.editorOptions.theme;
-    this.loadTheme(theme);
+    this.initializeEditorSettings(nextProps.editorSettings);
 
 
-    // set keymap
-    const keymapMode = nextProps.editorOptions.keymapMode;
-    this.setKeymapMode(keymapMode);
+    this.initializeTextlint(nextProps.isTextlintEnabled, nextProps.editorSettings);
 
 
     // fold drawio section
     // fold drawio section
     this.foldDrawioSection();
     this.foldDrawioSection();
   }
   }
 
 
-  async initializeTextlint() {
-    if (this.props.onInitializeTextlint != null) {
-      await this.props.onInitializeTextlint();
-      // If database has empty array, pass null instead to enable all default rules
-      const rulesForValidator = this.props.textlintRules?.length !== 0 ? this.props.textlintRules : null;
-      this.textlintValidator = createValidator(rulesForValidator);
-      this.codemirrorLintConfig = { getAnnotations: this.textlintValidator, async: true };
+  initializeEditorSettings(editorSettings) {
+    if (editorSettings == null) {
+      return;
+    }
+
+    // load theme
+    const theme = editorSettings.theme;
+    if (theme != null) {
+      this.loadTheme(theme);
+    }
+
+    // set keymap
+    const keymapMode = editorSettings.keymapMode;
+    if (keymapMode != null) {
+      this.setKeymapMode(keymapMode);
     }
     }
   }
   }
 
 
+  async initializeTextlint(isTextlintEnabled, editorSettings) {
+    if (!isTextlintEnabled || editorSettings == null) {
+      return;
+    }
+
+    const textlintRules = editorSettings.textlintSettings?.textlintRules;
+
+    // If database has empty array, pass null instead to enable all default rules
+    const rulesForValidator = (textlintRules == null || textlintRules.length === 0) ? null : textlintRules;
+    this.textlintValidator = createValidator(rulesForValidator);
+    this.codemirrorLintConfig = { getAnnotations: this.textlintValidator, async: true };
+  }
+
   getCodeMirror() {
   getCodeMirror() {
     return this.cm.editor;
     return this.cm.editor;
   }
   }
@@ -251,7 +262,6 @@ export default class CodeMirrorEditor extends AbstractEditor {
     // update state
     // update state
     this.setState({
     this.setState({
       isGfmMode: bool,
       isGfmMode: bool,
-      isEnabledEmojiAutoComplete: bool,
     });
     });
 
 
     this.updateCheatsheetStates(bool, null);
     this.updateCheatsheetStates(bool, null);
@@ -504,7 +514,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
     const context = {
     const context = {
       handlers: [], // list of handlers which process enter key
       handlers: [], // list of handlers which process enter key
       editor: this,
       editor: this,
-      editorOptions: this.props.editorOptions,
+      editorSettings: this.props.editorSettings,
     };
     };
 
 
     const interceptorManager = this.interceptorManager;
     const interceptorManager = this.interceptorManager;
@@ -568,9 +578,16 @@ export default class CodeMirrorEditor extends AbstractEditor {
 
 
     this.updateCheatsheetStates(null, value);
     this.updateCheatsheetStates(null, value);
 
 
-    // Emoji AutoComplete
-    if (this.state.isEnabledEmojiAutoComplete) {
-      this.emojiAutoCompleteHelper.showHint(editor);
+    // Show username hint on comment editor
+    if (this.props.isComment) {
+      this.commentMentionHelper.showUsernameHint();
+    }
+
+  }
+
+  keyUpHandler(editor, event) {
+    if (event.key !== 'Backspace') {
+      this.checkWhetherEmojiPickerShouldBeShown();
     }
     }
   }
   }
 
 
@@ -595,6 +612,26 @@ export default class CodeMirrorEditor extends AbstractEditor {
 
 
   }
   }
 
 
+  /**
+   * Show emoji picker component when emoji pattern (`:` + searchWord ) found
+   * eg `:a`, `:ap`
+   */
+  checkWhetherEmojiPickerShouldBeShown() {
+    const searchWord = this.emojiPickerHelper.getEmoji();
+
+    if (searchWord == null) {
+      this.setState({ isEmojiPickerShown: false });
+      this.setState({ emojiSearchText: null });
+    }
+    else {
+      this.setState({ emojiSearchText: searchWord });
+      // Show emoji picker after user stop typing
+      setTimeout(() => {
+        this.setState({ isEmojiPickerShown: true });
+      }, 700);
+    }
+  }
+
   /**
   /**
    * update states which related to cheatsheet
    * update states which related to cheatsheet
    * @param {boolean} isGfmModeTmp (use state.isGfmMode if null is set)
    * @param {boolean} isGfmModeTmp (use state.isGfmMode if null is set)
@@ -667,6 +704,24 @@ export default class CodeMirrorEditor extends AbstractEditor {
     );
     );
   }
   }
 
 
+  renderEmojiPicker() {
+    const { emojiSearchText } = this.state;
+    return this.state.isEmojiPickerShown
+      ? (
+        <div className="text-left">
+          <div className="mb-2 d-none d-md-block">
+            <EmojiPicker
+              onClose={() => this.setState({ isEmojiPickerShown: false, emojiSearchText: null })}
+              emojiSearchText={emojiSearchText}
+              emojiPickerHelper={this.emojiPickerHelper}
+              isOpen={this.state.isEmojiPickerShown}
+            />
+          </div>
+        </div>
+      )
+      : '';
+  }
+
   /**
   /**
    * return a function to replace a selected range with prefix + selection + suffix
    * return a function to replace a selected range with prefix + selection + suffix
    *
    *
@@ -750,6 +805,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
     this.drawioModal.current.show(mdu.getMarkdownDrawioMxfile(this.getCodeMirror()));
     this.drawioModal.current.show(mdu.getMarkdownDrawioMxfile(this.getCodeMirror()));
   }
   }
 
 
+
   // fold draw.io section (::: drawio ~ :::)
   // fold draw.io section (::: drawio ~ :::)
   foldDrawioSection() {
   foldDrawioSection() {
     const editor = this.getCodeMirror();
     const editor = this.getCodeMirror();
@@ -766,6 +822,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
     return range;
     return range;
   }
   }
 
 
+
   getNavbarItems() {
   getNavbarItems() {
     return [
     return [
       <Button
       <Button
@@ -903,11 +960,23 @@ export default class CodeMirrorEditor extends AbstractEditor {
       >
       >
         <EditorIcon icon="Drawio" />
         <EditorIcon icon="Drawio" />
       </Button>,
       </Button>,
+      <Button
+        key="nav-item-emoji"
+        color={null}
+        bssize="small"
+        title="Emoji"
+        onClick={() => this.setState({ isEmojiPickerShown: true })}
+      >
+        <EditorIcon icon="Emoji" />
+      </Button>,
     ];
     ];
   }
   }
 
 
+
   render() {
   render() {
-    const lint = this.props.isTextlintEnabled ? this.codemirrorLintConfig : false;
+    const { isTextlintEnabled } = this.props;
+
+    const lint = isTextlintEnabled ? this.codemirrorLintConfig : false;
     const additionalClasses = Array.from(this.state.additionalClassSet).join(' ');
     const additionalClasses = Array.from(this.state.additionalClassSet).join(' ');
     const placeholder = this.state.isGfmMode ? 'Input with Markdown..' : 'Input with Plain Text..';
     const placeholder = this.state.isGfmMode ? 'Input with Markdown..' : 'Input with Plain Text..';
 
 
@@ -915,7 +984,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
     if (this.props.lineNumbers != null) {
     if (this.props.lineNumbers != null) {
       gutters.push('CodeMirror-linenumbers', 'CodeMirror-foldgutter');
       gutters.push('CodeMirror-linenumbers', 'CodeMirror-foldgutter');
     }
     }
-    if (this.props.isTextlintEnabled === true) {
+    if (isTextlintEnabled) {
       gutters.push('CodeMirror-lint-markers');
       gutters.push('CodeMirror-lint-markers');
     }
     }
 
 
@@ -934,12 +1003,15 @@ export default class CodeMirrorEditor extends AbstractEditor {
           value={this.state.value}
           value={this.state.value}
           options={{
           options={{
             indentUnit: this.props.indentSize,
             indentUnit: this.props.indentSize,
+            theme: this.props.editorSettings.theme ?? 'elegant',
+            styleActiveLine: this.props.editorSettings.styleActiveLine,
             lineWrapping: true,
             lineWrapping: true,
             scrollPastEnd: true,
             scrollPastEnd: true,
             autoRefresh: { force: true }, // force option is enabled by autorefresh.ext.js -- Yuki Takei
             autoRefresh: { force: true }, // force option is enabled by autorefresh.ext.js -- Yuki Takei
             autoCloseTags: true,
             autoCloseTags: true,
             placeholder,
             placeholder,
             matchBrackets: true,
             matchBrackets: true,
+            emoji: true,
             matchTags: { bothTags: true },
             matchTags: { bothTags: true },
             // folding
             // folding
             foldGutter: this.props.lineNumbers,
             foldGutter: this.props.lineNumbers,
@@ -972,11 +1044,13 @@ export default class CodeMirrorEditor extends AbstractEditor {
               this.props.onDragEnter(event);
               this.props.onDragEnter(event);
             }
             }
           }}
           }}
+          onKeyUp={this.keyUpHandler}
         />
         />
 
 
         { this.renderLoadingKeymapOverlay() }
         { this.renderLoadingKeymapOverlay() }
 
 
         { this.renderCheatsheetOverlay() }
         { this.renderCheatsheetOverlay() }
+        { this.renderEmojiPicker() }
 
 
         <GridEditModal
         <GridEditModal
           ref={this.gridEditModal}
           ref={this.gridEditModal}
@@ -989,7 +1063,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
         <HandsontableModal
         <HandsontableModal
           ref={this.handsontableModal}
           ref={this.handsontableModal}
           onSave={(table) => { return mtu.replaceFocusedMarkdownTableWithEditor(this.getCodeMirror(), table) }}
           onSave={(table) => { return mtu.replaceFocusedMarkdownTableWithEditor(this.getCodeMirror(), table) }}
-          ignoreAutoFormatting={this.props.editorOptions.ignoreMarkdownTableAutoFormatting}
+          autoFormatMarkdownTable={this.props.editorSettings.autoFormatMarkdownTable}
         />
         />
         <DrawioModal
         <DrawioModal
           ref={this.drawioModal}
           ref={this.drawioModal}
@@ -1003,17 +1077,15 @@ export default class CodeMirrorEditor extends AbstractEditor {
 }
 }
 
 
 CodeMirrorEditor.propTypes = Object.assign({
 CodeMirrorEditor.propTypes = Object.assign({
-  editorOptions: PropTypes.object.isRequired,
   isTextlintEnabled: PropTypes.bool,
   isTextlintEnabled: PropTypes.bool,
-  textlintRules: PropTypes.array,
-  emojiStrategy: PropTypes.object,
   lineNumbers: PropTypes.bool,
   lineNumbers: PropTypes.bool,
+  editorSettings: PropTypes.object.isRequired,
   onMarkdownHelpButtonClicked: PropTypes.func,
   onMarkdownHelpButtonClicked: PropTypes.func,
   onAddAttachmentButtonClicked: PropTypes.func,
   onAddAttachmentButtonClicked: PropTypes.func,
-  onInitializeTextlint: PropTypes.func,
 }, AbstractEditor.propTypes);
 }, AbstractEditor.propTypes);
 
 
 CodeMirrorEditor.defaultProps = {
 CodeMirrorEditor.defaultProps = {
   lineNumbers: true,
   lineNumbers: true,
-  isTextlintEnabled: false,
 };
 };
+
+export default CodeMirrorEditor;

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