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

Merge branch 'support/apply-nextjs-2' into support/apply-nextjs-to-PageComments

jam411 3 лет назад
Родитель
Сommit
62cdab5419
100 измененных файлов с 1201 добавлено и 1305 удалено
  1. 32 1
      CHANGELOG.md
  2. 1 1
      lerna.json
  3. 1 1
      package.json
  4. 0 3
      packages/app-next/.eslintrc.json
  5. 0 35
      packages/app-next/.gitignore
  6. 0 20
      packages/app-next/tsconfig.json
  7. 2 2
      packages/app/docker/README.md
  8. 7 2
      packages/app/next.config.js
  9. 16 9
      packages/app/package.json
  10. 18 1
      packages/app/public/static/locales/en_US/admin.json
  11. 7 0
      packages/app/public/static/locales/en_US/translation.json
  12. 18 1
      packages/app/public/static/locales/ja_JP/admin.json
  13. 7 0
      packages/app/public/static/locales/ja_JP/translation.json
  14. 19 2
      packages/app/public/static/locales/zh_CN/admin.json
  15. 7 0
      packages/app/public/static/locales/zh_CN/translation.json
  16. 1 1
      packages/app/resource/locales/en_US/welcome.md
  17. 1 1
      packages/app/resource/locales/ja_JP/welcome.md
  18. 1 1
      packages/app/resource/locales/zh_CN/welcome.md
  19. 6 6
      packages/app/src/client/admin.jsx
  20. 0 4
      packages/app/src/client/boot.js
  21. 1 5
      packages/app/src/client/services/AdminAppContainer.js
  22. 1 4
      packages/app/src/client/services/AdminCustomizeContainer.js
  23. 1 4
      packages/app/src/client/services/AdminImportContainer.js
  24. 1 4
      packages/app/src/client/services/AdminMarkDownContainer.js
  25. 1 3
      packages/app/src/client/services/AdminSlackIntegrationLegacyContainer.js
  26. 1 0
      packages/app/src/client/services/PageContainer.js
  27. 0 73
      packages/app/src/client/util/color-scheme.js
  28. 0 51
      packages/app/src/components/Admin/App/AppSettingsPage.jsx
  29. 22 1
      packages/app/src/components/Admin/App/AppSettingsPageContents.tsx
  30. 1 1
      packages/app/src/components/Admin/App/FileUploadSetting.tsx
  31. 1 2
      packages/app/src/components/Admin/App/SesSetting.tsx
  32. 1 2
      packages/app/src/components/Admin/App/SmtpSetting.tsx
  33. 0 56
      packages/app/src/components/Admin/CustomCssEditor.jsx
  34. 0 57
      packages/app/src/components/Admin/CustomHeaderEditor.jsx
  35. 0 56
      packages/app/src/components/Admin/CustomScriptEditor.jsx
  36. 25 26
      packages/app/src/components/Admin/Customize/Customize.jsx
  37. 5 6
      packages/app/src/components/Admin/Customize/CustomizeCssSetting.tsx
  38. 1 3
      packages/app/src/components/Admin/Customize/CustomizeFunctionSetting.tsx
  39. 4 3
      packages/app/src/components/Admin/Customize/CustomizeHeaderSetting.tsx
  40. 5 6
      packages/app/src/components/Admin/Customize/CustomizeLayoutSetting.tsx
  41. 185 0
      packages/app/src/components/Admin/Customize/CustomizeLogoSetting.tsx
  42. 4 3
      packages/app/src/components/Admin/Customize/CustomizeScriptSetting.tsx
  43. 4 5
      packages/app/src/components/Admin/Customize/CustomizeSidebarSetting.tsx
  44. 21 18
      packages/app/src/components/Admin/Customize/CustomizeThemeOptions.jsx
  45. 2 13
      packages/app/src/components/Admin/Customize/CustomizeThemeSetting.tsx
  46. 6 6
      packages/app/src/components/Admin/Customize/CustomizeTitle.jsx
  47. 1 0
      packages/app/src/components/Admin/Customize/ThemeColorBox.jsx
  48. 0 245
      packages/app/src/components/Admin/ElasticsearchManagement/ElasticsearchManagement.jsx
  49. 212 0
      packages/app/src/components/Admin/ElasticsearchManagement/ElasticsearchManagement.tsx
  50. 22 26
      packages/app/src/components/Admin/ElasticsearchManagement/RebuildIndexControls.jsx
  51. 4 2
      packages/app/src/components/Admin/ExportArchiveData/SelectCollectionsModal.jsx
  52. 1 1
      packages/app/src/components/Admin/ExportArchiveDataPage.jsx
  53. 23 1
      packages/app/src/components/Admin/ImportData/ImportDataPageContents.jsx
  54. 0 52
      packages/app/src/components/Admin/ImportDataPage.jsx
  55. 25 28
      packages/app/src/components/Admin/LegacySlackIntegration/LegacySlackIntegration.jsx
  56. 1 1
      packages/app/src/components/Admin/LegacySlackIntegration/SlackConfiguration.jsx
  57. 0 48
      packages/app/src/components/Admin/MarkdownSetting/MarkDownSetting.jsx
  58. 36 3
      packages/app/src/components/Admin/MarkdownSetting/MarkDownSettingContents.tsx
  59. 2 2
      packages/app/src/components/Admin/Notification/ManageGlobalNotification.jsx
  60. 4 4
      packages/app/src/components/Admin/Security/BasicSecuritySetting.jsx
  61. 5 4
      packages/app/src/components/Admin/Security/GitHubSecuritySetting.jsx
  62. 5 4
      packages/app/src/components/Admin/Security/GoogleSecuritySetting.jsx
  63. 4 4
      packages/app/src/components/Admin/Security/LdapSecuritySetting.jsx
  64. 4 4
      packages/app/src/components/Admin/Security/LocalSecuritySetting.jsx
  65. 4 4
      packages/app/src/components/Admin/Security/OidcSecuritySetting.jsx
  66. 5 5
      packages/app/src/components/Admin/Security/SamlSecuritySetting.jsx
  67. 5 4
      packages/app/src/components/Admin/Security/SecurityManagement.jsx
  68. 5 4
      packages/app/src/components/Admin/Security/TwitterSecuritySetting.jsx
  69. 1 1
      packages/app/src/components/Admin/SlackIntegration/BotTypeCard.jsx
  70. 2 1
      packages/app/src/components/Admin/SlackIntegration/Bridge.jsx
  71. 3 2
      packages/app/src/components/Admin/SlackIntegration/ConfirmBotChangeModal.jsx
  72. 7 11
      packages/app/src/components/Admin/SlackIntegration/CustomBotWithProxySettings.jsx
  73. 3 8
      packages/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySecretTokenSection.jsx
  74. 11 9
      packages/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySettings.jsx
  75. 3 9
      packages/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx
  76. 8 11
      packages/app/src/components/Admin/SlackIntegration/OfficialBotSettings.jsx
  77. 2 13
      packages/app/src/components/Admin/SlackIntegration/SlackIntegration.jsx
  78. 8 9
      packages/app/src/components/Admin/SlackIntegration/WithProxyAccordions.jsx
  79. 2 2
      packages/app/src/components/Admin/UserGroup/UserGroupDeleteModal.tsx
  80. 2 2
      packages/app/src/components/Admin/UserGroupDetail/UserGroupUserTable.jsx
  81. 0 49
      packages/app/src/components/BasicLayout.tsx
  82. 1 1
      packages/app/src/components/BookmarkButtons.tsx
  83. 1 1
      packages/app/src/components/Common/Dropdown/PageItemControl.tsx
  84. 127 0
      packages/app/src/components/Common/ImageCropModal.tsx
  85. 30 7
      packages/app/src/components/Drawio.tsx
  86. 23 23
      packages/app/src/components/InstallerForm.jsx
  87. 14 18
      packages/app/src/components/Layout/Admin.module.scss
  88. 9 2
      packages/app/src/components/Layout/AdminLayout.tsx
  89. 58 0
      packages/app/src/components/Layout/BasicLayout.tsx
  90. 48 0
      packages/app/src/components/Layout/RawLayout.tsx
  91. 1 1
      packages/app/src/components/LikeButtons.tsx
  92. 2 2
      packages/app/src/components/LoginForm.jsx
  93. 1 1
      packages/app/src/components/Me/DisassociateModal.tsx
  94. 2 2
      packages/app/src/components/Me/ExternalAccountRow.jsx
  95. 0 125
      packages/app/src/components/Me/ImageCropModal.jsx
  96. 3 2
      packages/app/src/components/Me/ProfileImageSettings.tsx
  97. 12 28
      packages/app/src/components/Navbar/AppearanceModeDropdown.tsx
  98. 2 4
      packages/app/src/components/Navbar/GlobalSearch.tsx
  99. 5 3
      packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  100. 5 13
      packages/app/src/components/Navbar/GrowiNavbar.tsx

+ 32 - 1
CHANGELOG.md

@@ -1,9 +1,40 @@
 # Changelog
 
-## [Unreleased](https://github.com/weseek/growi/compare/v5.0.11...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v5.1.0...HEAD)
 
 *Please do not manually update this file. We've automated the process.*
 
+## [v5.1.0](https://github.com/weseek/growi/compare/v5.0.11...v5.1.0) - 2022-07-21
+
+### 💎 Features
+
+- feat: Custom brand logo image (#5709) @mudana-grune
+- feat: Rate Limit by rate-limit-flexible (#6053) @yukendev
+- feat: Audit Log (#5915) @miya
+
+### 🚀 Improvement
+
+- imprv: Prevent XSS with React (#6274) @yuki-takei
+- imprv: Reflect tmp tag data (#6124) @kaoritokashiki
+- imprv: Update subscribe button icon on Navbar (#6213) @jam411
+- imprv: Event emittion by socket.io is triggered only when ES reindexing (#6077) @hirokei-camel
+
+### 🐛 Bug Fixes
+
+- fix: Drawio rendering (#6275) @hakumizuki
+- fix: Blink section header on init (#6249) @yuki-takei
+- fix: Error when trying login with an email that contains plus sign (#6232) @miya
+- fix: Use APIv3 for api get check_username (#6226) @kaoritokashiki
+- fix: Slack integration connection test (#6201) @yukendev
+- fix: Not found page for `/${ObjectId like string}` path (#6208) @yuki-takei
+
+### 🧰 Maintenance
+
+- support: Refactor PageInfo types (#6283) @yuki-takei
+- support: Refactor growi renderer using hooks 2 (#6237) @yuki-takei
+- support: Refactor growi renderer using hooks (#6223) @hakumizuki
+- imprv: Omit Personal Container (#6182) @kaoritokashiki
+
 ## [v5.0.11](https://github.com/weseek/growi/compare/v5.0.10...v5.0.11) - 2022-07-05
 
 ### 💎 Features

+ 1 - 1
lerna.json

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

+ 1 - 1
package.json

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

+ 0 - 3
packages/app-next/.eslintrc.json

@@ -1,3 +0,0 @@
-{
-  "extends": "next/core-web-vitals"
-}

+ 0 - 35
packages/app-next/.gitignore

@@ -1,35 +0,0 @@
-# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
-
-# dependencies
-/node_modules
-/.pnp
-.pnp.js
-
-# testing
-/coverage
-
-# next.js
-/.next/
-/out/
-
-# production
-/build
-
-# misc
-.DS_Store
-*.pem
-
-# debug
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-.pnpm-debug.log*
-
-# local env files
-.env*.local
-
-# vercel
-.vercel
-
-# typescript
-*.tsbuildinfo

+ 0 - 20
packages/app-next/tsconfig.json

@@ -1,20 +0,0 @@
-{
-  "compilerOptions": {
-    "target": "es5",
-    "lib": ["dom", "dom.iterable", "esnext"],
-    "allowJs": true,
-    "skipLibCheck": true,
-    "strict": true,
-    "forceConsistentCasingInFileNames": true,
-    "noEmit": true,
-    "esModuleInterop": true,
-    "module": "esnext",
-    "moduleResolution": "node",
-    "resolveJsonModule": true,
-    "isolatedModules": true,
-    "jsx": "preserve",
-    "incremental": true
-  },
-  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
-  "exclude": ["node_modules"]
-}

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

@@ -10,8 +10,8 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 
-* [`5.1.0`, `5.1`, `5`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v5.1.0/packages/app/docker/Dockerfile)
-* [`5.1.0-nocdn`, `5.1-nocdn`, `5-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v5.1.0/packages/app/docker/Dockerfile)
+* [`5.1.0`, `5.1`, `5`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v5.1.0/docker/Dockerfile)
+* [`5.1.0-nocdn`, `5.1-nocdn`, `5-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v5.1.0/docker/Dockerfile)
 * [`5.0.11`, `5.0` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.11/packages/app/docker/Dockerfile)
 * [`5.0.11-nocdn`, `5.0-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.11/packages/app/docker/Dockerfile)
 * [`4.5.23`, `4.5`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.23/packages/app/docker/Dockerfile)

+ 7 - 2
packages/app/next.config.js

@@ -1,5 +1,6 @@
 import eazyLogger from 'eazy-logger';
 import { I18NextHMRPlugin } from 'i18next-hmr/plugin';
+import { withSuperjson } from 'next-superjson';
 import { WebpackManifestPlugin } from 'webpack-manifest-plugin';
 
 import { i18n, localePath } from './src/next-i18next.config';
@@ -22,8 +23,13 @@ const setupWithTM = () => {
     'unified',
     'comma-separated-tokens',
     'decode-named-character-reference',
+    'html-void-elements',
+    'property-information',
     'space-separated-tokens',
     'trim-lines',
+    'web-namespaces',
+    'vfile',
+    'zwitch',
     'emoticon',
     ...listPrefixedPackages(['remark-', 'rehype-', 'hast-', 'mdast-', 'micromark-', 'micromark-', 'unist-']),
   ];
@@ -58,7 +64,6 @@ const nextConfig = {
 
   /** @param config {import('next').NextConfig} */
   webpack(config, options) {
-
     // Avoid "Module not found: Can't resolve 'fs'"
     // See: https://stackoverflow.com/a/68511591
     if (!options.isServer) {
@@ -93,4 +98,4 @@ const nextConfig = {
 
 };
 
-module.exports = withTM(nextConfig);
+module.exports = withSuperjson()(withTM(nextConfig));

+ 16 - 9
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "5.1.0-RC.2",
+  "version": "5.1.1-RC.0",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -63,11 +63,11 @@
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^5.1.0-RC.2",
-    "@growi/plugin-attachment-refs": "^5.1.0-RC.2",
-    "@growi/plugin-lsx": "^5.1.0-RC.2",
-    "@growi/plugin-pukiwiki-like-linker": "^5.1.0-RC.2",
-    "@growi/slack": "^5.1.0-RC.2",
+    "@growi/codemirror-textlint": "^5.1.1-RC.0",
+    "@growi/plugin-attachment-refs": "^5.1.1-RC.0",
+    "@growi/plugin-lsx": "^5.1.1-RC.0",
+    "@growi/plugin-pukiwiki-like-linker": "^5.1.1-RC.0",
+    "@growi/slack": "^5.1.1-RC.0",
     "@promster/express": "^7.0.2",
     "@promster/server": "^7.0.4",
     "@slack/events-api": "^3.0.0",
@@ -126,7 +126,7 @@
     "multer-autoreap": "^1.0.3",
     "next": "^12.1.6",
     "next-i18next": "^11.0.0",
-    "next-transpile-modules": "^9.0.0",
+    "next-themes": "^0.2.0",
     "nocache": "^3.0.1",
     "nodemailer": "^6.6.2",
     "nodemailer-ses-transport": "~1.5.0",
@@ -153,16 +153,18 @@
     "react-multiline-clamp": "^2.0.0",
     "reconnecting-websocket": "^4.4.0",
     "redis": "^3.0.2",
+    "rehype-raw": "^6.1.1",
+    "rehype-sanitize": "^5.0.1",
     "rehype-slug": "^5.0.1",
     "rehype-toc": "^3.0.2",
     "remark-breaks": "^3.0.2",
     "remark-emoji": "^3.0.2",
-    "remark-footnotes": "^4.0.1",
     "remark-gfm": "^3.0.1",
     "rimraf": "^3.0.0",
     "socket.io": "^4.2.0",
     "stream-to-promise": "^3.0.0",
     "string-width": "=4.2.2",
+    "superjson": "^1.9.1",
     "swagger-jsdoc": "^6.1.0",
     "swig-templates": "^2.0.2",
     "uglifycss": "^0.0.29",
@@ -179,13 +181,14 @@
   },
   "devDependencies": {
     "@alienfast/i18next-loader": "^1.1.4",
-    "@growi/ui": "^5.1.0-RC.2",
+    "@growi/ui": "^5.1.1-RC.0",
     "@handsontable/react": "=2.1.0",
     "@types/compression": "^1.7.0",
     "@types/express": "^4.17.11",
     "@types/jquery": "^3.5.8",
     "@types/multer": "^1.4.5",
     "autoprefixer": "^9.0.0",
+    "babel-loader": "^8.2.5",
     "bootstrap": "^4.6.1",
     "browser-sync": "^2.27.7",
     "bunyan-debug": "^2.0.0",
@@ -200,6 +203,7 @@
     "emoji-mart": "npm:panta82-emoji-mart@^3.0.1",
     "eslint-plugin-cypress": "^2.12.1",
     "eslint-plugin-regex": "^1.8.0",
+    "font-awesome": "^4.7.0",
     "handsontable": "=6.2.2",
     "i18next-hmr": "^1.7.7",
     "jquery-slimscroll": "^1.3.8",
@@ -210,6 +214,8 @@
     "markdown-table": "^1.1.1",
     "material-icons": "^1.11.3",
     "morgan": "^1.10.0",
+    "next-superjson": "^0.0.4",
+    "next-transpile-modules": "^9.0.0",
     "normalize-path": "^3.0.0",
     "penpal": "^4.0.0",
     "plantuml-encoder": "^1.2.5",
@@ -233,6 +239,7 @@
     "sticky-events": "^3.4.11",
     "swagger2openapi": "^5.3.1",
     "swr": "^1.3.0",
+    "@icon/themify-icons": "1.0.1-alpha.3",
     "throttle-debounce": "^3.0.1",
     "toastr": "^2.1.2",
     "ts-node-dev": "^2.0.0",

+ 18 - 1
packages/app/public/static/locales/en_US/admin/admin.json → packages/app/public/static/locales/en_US/admin.json

@@ -207,7 +207,13 @@
     "ctrl_space": "Ctrl+Space to autocomplete",
     "custom_script": "Custom script",
     "write_java": "You can write Javascript that is applied to whole system.",
-    "reflect_change": "You need to reload the page to reflect the change."
+    "reflect_change": "You need to reload the page to reflect the change.",
+    "custom_logo" : "Custom Logo",
+    "default_logo": "Default Logo",
+    "upload_logo": "Upload Logo",
+    "current_logo": "Current Logo",
+    "upload_new_logo": "Upload New Logo",
+    "delete_logo": "Delete Logo"
   },
   "importer_management": {
     "beta_warning": "This function is Beta.",
@@ -560,6 +566,8 @@
     "USER_LOGIN_WITH_BASIC": "Login with BASIC",
     "USER_LOGIN_FAILURE": "Login failure",
     "USER_LOGOUT": "Logout",
+    "USER_FOGOT_PASSWORD": "Request password reset",
+    "USER_RESET_PASSWORD": "Reset password",
     "USER_PERSONAL_SETTINGS_UPDATE": "User personal settings update",
     "USER_IMAGE_TYPE_UPDATE": "User image type update",
     "USER_LDAP_ACCOUNT_ASSOCIATE": "LDAP account associate",
@@ -647,6 +655,7 @@
     "ADMIN_MARKDOWN_XSS_UPDATE": "Update prevent XSS settings",
     "ADMIN_LAYOUT_UPDATE": "Update Layout",
     "ADMIN_THEME_UPDATE": "Update Theme",
+    "ADMIN_SIDEBAR_UPDATE": "Update Default Sidebar mode",
     "ADMIN_FUNCTION_UPDATE": "Update Function",
     "ADMIN_CODE_HIGHLIGHT_UPDATE": "Update Code Highlight",
     "ADMIN_CUSTOM_TITLE_UPDATE": "Update Custom Title",
@@ -687,10 +696,18 @@
     "ADMIN_SLACK_WITHOUT_PROXY_TEST": "Test connection to Slack bot without proxy",
     "ADMIN_SLACK_CONFIGURATION_SETTING_UPDATE": "Update Slack Incoming Webhooks configuration",
     "ADMIN_USERS_INVITE": "User Invitation",
+    "ADMIN_USERS_PASSWORD_RESET": "Reset user password",
+    "ADMIN_USERS_ACTIVATE": "Activate user",
+    "ADMIN_USERS_DEACTIVATE": "Deactivate user",
+    "ADMIN_USERS_GIVE_ADMIN": "Give admin access",
+    "ADMIN_USERS_REMOVE_ADMIN": "Remove admin access",
+    "ADMIN_USERS_SEND_INVITATION_EMAIL": "Resend invitation email",
+    "ADMIN_USERS_REMOVE": "Remove user",
     "ADMIN_USER_GROUP_CREATE": "Create User Group",
     "ADMIN_USER_GROUP_UPDATE": "Update User Group",
     "ADMIN_USER_GROUP_DELETE": "Delete User Group",
     "ADMIN_USER_GROUP_ADD_USER": "Add User to User Group",
+    "ADMIN_SEARCH_CONNECTION": "Attempting to reconnect to Elasticsearch",
     "ADMIN_SEARCH_INDICES_NORMALIZE": "Normalize of Elasticsearch indexes",
     "ADMIN_SEARCH_INDICES_REBUILD": "Rebuild Elasticsearch indexes"
   }

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

@@ -30,6 +30,7 @@
   "New": "New",
   "Close": "Close",
   "Shortcuts": "Shortcuts",
+  "CustomSidebar": "Custom Sidebar",
   "eg": "e.g.",
   "add": "Add",
   "Undo": "Undo",
@@ -1089,6 +1090,12 @@
     "belonging_to_no_group": "Could not find the groups you belong to.",
     "manage_user_groups": "Manage user groups"
   },
+  "crop_image_modal": {
+    "image_crop": "Image Crop",
+    "crop": "Crop",
+    "reset": "Reset",
+    "cancel": "Cancel"
+  },
   "fix_page_grant": {
     "modal": {
       "no_grant_available": "The list of selectable permissions could not be found. Please modify the permissions on the parent page first and try again.",

+ 18 - 1
packages/app/public/static/locales/ja_JP/admin/admin.json → packages/app/public/static/locales/ja_JP/admin.json

@@ -207,7 +207,13 @@
     "ctrl_space": "Ctrl+Space でコード補完",
     "custom_script": "カスタムスクリプト",
     "write_java": "システム全体に適用されるJavaScriptを記述できます。",
-    "reflect_change": "変更の反映はページの更新が必要です。"
+    "reflect_change": "変更の反映はページの更新が必要です。",
+    "custom_logo": "カスタムロゴ",
+    "default_logo": "デフォルトのロゴ",
+    "upload_logo": "ロゴをアップロード",
+    "current_logo": "現在のロゴ",
+    "upload_new_logo": "新しいロゴをアップロードする",
+    "delete_logo": "ロゴを削除"
   },
   "export_management": {
     "exporting_collection_list": "エクスポート中のコレクション",
@@ -559,6 +565,8 @@
     "USER_LOGIN_WITH_BASIC": "BASIC 認証でログイン",
     "USER_LOGIN_FAILURE": "ログイン失敗",
     "USER_LOGOUT": "ログアウト",
+    "USER_FOGOT_PASSWORD": "パスワードリセットのリクエスト",
+    "USER_RESET_PASSWORD": "パスワードのリセット",
     "USER_PERSONAL_SETTINGS_UPDATE": "ユーザーの基本情報の更新",
     "USER_IMAGE_TYPE_UPDATE": "プロフィール画像の変更",
     "USER_LDAP_ACCOUNT_ASSOCIATE": "LDAP アカウントの追加",
@@ -646,6 +654,7 @@
     "ADMIN_MARKDOWN_XSS_UPDATE": "XSS 対策設定の更新",
     "ADMIN_LAYOUT_UPDATE": "レイアウト設定の更新",
     "ADMIN_THEME_UPDATE": "テーマ設定の更新",
+    "ADMIN_SIDEBAR_UPDATE": "デフォルトのサイドバーモードの設定の更新",
     "ADMIN_FUNCTION_UPDATE": "機能設定の更新",
     "ADMIN_CODE_HIGHLIGHT_UPDATE": "コードハイライト設定の更新",
     "ADMIN_CUSTOM_TITLE_UPDATE": "カスタムタイトル設定の更新",
@@ -686,10 +695,18 @@
     "ADMIN_SLACK_WITHOUT_PROXY_TEST": "Slack bot without proxy の接続テスト",
     "ADMIN_SLACK_CONFIGURATION_SETTING_UPDATE": "Slack Incoming Webhooks の設定の更新",
     "ADMIN_USERS_INVITE": "ユーザーの招待",
+    "ADMIN_USERS_PASSWORD_RESET": "ユーザーのパスワードをリセット",
+    "ADMIN_USERS_ACTIVATE": "ユーザーを承認する",
+    "ADMIN_USERS_DEACTIVATE": "ユーザーを停止する",
+    "ADMIN_USERS_GIVE_ADMIN": "管理者にする",
+    "ADMIN_USERS_REMOVE_ADMIN": "管理者から外す",
+    "ADMIN_USERS_SEND_INVITATION_EMAIL": "招待メールの再送信",
+    "ADMIN_USERS_REMOVE": "ユーザーの削除",
     "ADMIN_USER_GROUP_CREATE": "ユーザーグループの作成",
     "ADMIN_USER_GROUP_UPDATE": "ユーザーグループの更新",
     "ADMIN_USER_GROUP_DELETE": "ユーザーグループの削除",
     "ADMIN_USER_GROUP_ADD_USER": "ユーザーグループにユーザーを追加",
+    "ADMIN_SEARCH_CONNECTION": "Elasticsearch の再接続の試行",
     "ADMIN_SEARCH_INDICES_NORMALIZE": "Elasticsearch のインデックスの正規化",
     "ADMIN_SEARCH_INDICES_REBUILD": "Elasticsearch のインデックスのリビルド"
   }

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

@@ -30,6 +30,7 @@
   "New": "作成",
   "Close": "閉じる",
   "Shortcuts": "ショートカット",
+  "CustomSidebar": "カスタムサイドバー",
   "eg": "例:",
   "add": "追加",
   "Undo": "元に戻す",
@@ -1082,6 +1083,12 @@
     "belonging_to_no_group": "所属しているグループが見つかりませんでした。",
     "manage_user_groups": "グループ管理"
   },
+  "crop_image_modal": {
+    "image_crop": "画像の切り抜き",
+    "crop": "トリミング",
+    "reset": "リセット",
+    "cancel": "キャンセル"
+  },
   "fix_page_grant": {
     "modal": {
       "no_grant_available": "選択可能な権限のリストが見つかりませんでした。まず親ページの権限を修正したのちに再試行してください。",

+ 19 - 2
packages/app/public/static/locales/zh_CN/admin/admin.json → packages/app/public/static/locales/zh_CN/admin.json

@@ -217,7 +217,13 @@
     "ctrl_space": "Ctrl+Space 自动完成",
     "custom_script": "定制纸条",
     "write_java": "您可以编写应用于整个系统的Javascript。",
-    "reflect_change": "您需要重新加载页面以反映更改。"
+    "reflect_change": "您需要重新加载页面以反映更改。",
+    "custom_logo": "自定义徽标",
+    "default_logo": "默认徽标",
+    "upload_logo": "上传徽标",
+    "current_logo": "当前标志",
+    "upload_new_logo": "上传新徽标",
+    "delete_logo": "删除徽标"
   },
   "importer_management": {
     "beta_warning": "这个函数是Beta。",
@@ -569,6 +575,8 @@
     "USER_LOGIN_WITH_BASIC": "使用 BASIC 登录",
     "USER_LOGIN_FAILURE": "登录失败",
     "USER_LOGOUT": "注销",
+    "USER_FOGOT_PASSWORD": "要求重置密码",
+    "USER_RESET_PASSWORD": "重置密码",
     "USER_PERSONAL_SETTINGS_UPDATE": "用户个人设置更新",
     "USER_IMAGE_TYPE_UPDATE": "用户图片类型更新",
     "USER_LDAP_ACCOUNT_ASSOCIATE": "LDAP 帐户关联",
@@ -656,6 +664,7 @@
     "ADMIN_MARKDOWN_XSS_UPDATE": "更新阻止 XSS 设置",
     "ADMIN_LAYOUT_UPDATE": "更新布局",
     "ADMIN_THEME_UPDATE": "更新主题",
+    "ADMIN_SIDEBAR_UPDATE": "更新默认的侧边栏模式",
     "ADMIN_FUNCTION_UPDATE": "更新函数",
     "ADMIN_CODE_HIGHLIGHT_UPDATE": "更新代码高亮",
     "ADMIN_CUSTOM_TITLE_UPDATE": "更新自定义标题",
@@ -696,11 +705,19 @@
     "ADMIN_SLACK_WITHOUT_PROXY_TEST": "在没有代理的情况下测试与 Slack 机器人的连接",
     "ADMIN_SLACK_CONFIGURATION_SETTING_UPDATE": "更新 Slack Incoming Webhooks 配置",
     "ADMIN_USERS_INVITE": "用户邀请",
+    "ADMIN_USERS_PASSWORD_RESET": "重置用户密码",
+    "ADMIN_USERS_ACTIVATE": "激活用户",
+    "ADMIN_USERS_DEACTIVATE": "停用用户",
+    "ADMIN_USERS_GIVE_ADMIN": "授予管理员访问权限",
+    "ADMIN_USERS_REMOVE_ADMIN": "删除管理员访问权限",
+    "ADMIN_USERS_SEND_INVITATION_EMAIL": "重发邀请函",
+    "ADMIN_USERS_REMOVE": "删除用户",
     "ADMIN_USER_GROUP_CREATE": "创建用户组",
     "ADMIN_USER_GROUP_UPDATE": "更新用户组",
     "ADMIN_USER_GROUP_DELETE": "删除用户组",
     "ADMIN_USER_GROUP_ADD_USER": "添加用户到用户组",
-    "ADMIN_SEARCH_INDICES_NORMALIZE": "Elasticsearch 索引的规范化",
+    "ADMIN_SEARCH_CONNECTION": "重试Elasticsearch连接",
+    "ADMIN_SEARCH_INDICES_NORMALIZE": "试图重新连接Elasticsearch",
     "ADMIN_SEARCH_INDICES_REBUILD": "重建 Elasticsearch 索引"
   }
 }

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

@@ -31,6 +31,7 @@
   "New": "新建",
   "Close": "Close",
 	"Shortcuts": "快捷方式",
+  "CustomSidebar": "Custom Sidebar",
 	"eg": "e.g.",
 	"add": "添加",
 	"Undo": "撤销",
@@ -1092,6 +1093,12 @@
     "belonging_to_no_group": "无法找到你所属的团体。",
     "manage_user_groups": "管理用户组"
   },
+  "crop_image_modal": {
+    "image_crop": "图像裁剪",
+    "crop": "修剪",
+    "reset": "重启",
+    "cancel": "取消"
+  },
   "fix_page_grant": {
     "modal": {
       "no_grant_available": "无法找到可选择的权限列表。 请先修改父页的权限,然后再试一次。",

+ 1 - 1
packages/app/resource/locales/en_US/welcome.md

@@ -1,7 +1,7 @@
 # :tada: Welcome to GROWI
 
 [![GitHub Releases](https://img.shields.io/github/release/weseek/growi.svg)](https://github.com/weseek/growi/releases/latest)
-[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)
+[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://github.com/weseek/growi/blob/master/LICENSE)
 
 GROWI is a Wiki for Individuals and Corporations | A knowledge base tool.
 Knowledge in companies, university laboratories, and clubs can be easily shared and anyone can edit the page.

+ 1 - 1
packages/app/resource/locales/ja_JP/welcome.md

@@ -1,6 +1,6 @@
 # :tada: GROWI へようこそ
 [![GitHub Releases](https://img.shields.io/github/release/weseek/growi.svg)](https://github.com/weseek/growi/releases/latest)
-[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)
+[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://github.com/weseek/growi/blob/master/LICENSE)
 
 GROWI は個人・法人向けの Wiki | ナレッジベースツールです。  
 会社や大学の研究室、サークルでのナレッジ情報を簡単に共有でき、作られたページは誰でも編集が可能です。

+ 1 - 1
packages/app/resource/locales/zh_CN/welcome.md

@@ -1,7 +1,7 @@
 # :tada: 欢迎来到GROWI
 
 [![GitHub Releases](https://img.shields.io/github/release/weseek/growi.svg)](https://github.com/weseek/growi/releases/latest)
-[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)
+[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://github.com/weseek/growi/blob/master/LICENSE)
 
 GROWI是一个针对个人和公司的Wiki - 一个知识库工具。
 公司、大学实验室和俱乐部的知识可以轻松共享,任何人都可以编辑页面。

+ 6 - 6
packages/app/src/client/admin.jsx

@@ -30,16 +30,16 @@ import loggerFactory from '~/utils/logger';
 import { swrGlobalConfiguration } from '~/utils/swr-utils';
 
 import AdminHome from '../components/Admin/AdminHome/AdminHome';
-import AppSettingsPage from '../components/Admin/App/AppSettingsPage';
+// import AppSettingsPage from '../components/Admin/App/AppSettingsPage';
 import { AuditLogManagement } from '../components/Admin/AuditLogManagement';
 import AdminNavigation from '../components/Admin/Common/AdminNavigation';
 import Customize from '../components/Admin/Customize/Customize';
 import ExportArchiveDataPage from '../components/Admin/ExportArchiveDataPage';
 import FullTextSearchManagement from '../components/Admin/FullTextSearchManagement';
-import ImportDataPage from '../components/Admin/ImportDataPage';
+// import ImportDataPage from '../components/Admin/ImportDataPage';
 import LegacySlackIntegration from '../components/Admin/LegacySlackIntegration/LegacySlackIntegration';
 import ManageExternalAccount from '../components/Admin/ManageExternalAccount';
-import MarkdownSetting from '../components/Admin/MarkdownSetting/MarkDownSetting';
+// import MarkdownSetting from '../components/Admin/MarkdownSetting/MarkDownSetting';
 import ManageGlobalNotification from '../components/Admin/Notification/ManageGlobalNotification';
 import NotificationSetting from '../components/Admin/Notification/NotificationSetting';
 import SecurityManagement from '../components/Admin/Security/SecurityManagement';
@@ -94,10 +94,10 @@ logger.info('unstated containers have been initialized');
  */
 Object.assign(componentMappings, {
   'admin-home': <AdminHome />,
-  'admin-app': <AppSettingsPage />,
-  'admin-markdown-setting': <MarkdownSetting />,
+  // 'admin-app': <AppSettingsPage />,
+  // 'admin-markdown-setting': <MarkdownSetting />,
   'admin-customize': <Customize />,
-  'admin-importer': <ImportDataPage />,
+  // 'admin-importer': <ImportDataPage />,
   'admin-export-page': <ExportArchiveDataPage />,
   'admin-notification-setting': <NotificationSetting />,
   'admin-slack-integration': <SlackIntegration />,

+ 0 - 4
packages/app/src/client/boot.js

@@ -1,9 +1,5 @@
-import {
-  applyColorScheme,
-} from './util/color-scheme';
 import {
   applyOldIos,
 } from './util/old-ios';
 
-applyColorScheme();
 applyOldIos();

+ 1 - 5
packages/app/src/client/services/AdminAppContainer.js

@@ -11,13 +11,9 @@ export default class AdminAppContainer extends Container {
   constructor() {
     super();
 
-    this.dummyTitle = 0;
-    this.dummyTitleForError = 1;
-
     this.state = {
       retrieveError: null,
-      // set dummy value tile for using suspense
-      title: this.dummyTitle,
+      title: '',
       confidential: '',
       globalLang: '',
       isEmailPublishedForNewUser: true,

+ 1 - 4
packages/app/src/client/services/AdminCustomizeContainer.js

@@ -17,13 +17,10 @@ export default class AdminCustomizeContainer extends Container {
   constructor() {
     super();
 
-    this.dummyCurrentTheme = 0;
-    this.dummyCurrentThemeForError = 1;
-
     this.state = {
       retrieveError: null,
       // set dummy value tile for using suspense
-      currentTheme: this.dummyCurrentTheme,
+      currentTheme: 'default',
       isEnabledTimeline: false,
       isSavedStatesOfTabChanges: false,
       isEnabledAttachTitleHeader: false,

+ 1 - 4
packages/app/src/client/services/AdminImportContainer.js

@@ -18,13 +18,10 @@ export default class AdminImportContainer extends Container {
     super();
 
     this.appContainer = appContainer;
-    this.dummyEsaTeamName = 0;
-    this.dummyEsaTeamNameForError = 1;
 
     this.state = {
       retrieveError: null,
-      // set dummy value tile for using suspense
-      esaTeamName: this.dummyEsaTeamName,
+      esaTeamName: '',
       esaAccessToken: '',
       qiitaTeamName: '',
       qiitaAccessToken: '',

+ 1 - 4
packages/app/src/client/services/AdminMarkDownContainer.js

@@ -12,13 +12,10 @@ export default class AdminMarkDownContainer extends Container {
     super();
 
     this.appContainer = appContainer;
-    this.dummyIsEnabledLinebreaks = 0;
-    this.dummyIsEnabledLinebreaksForError = 1;
 
     this.state = {
       retrieveError: null,
-      // set dummy value tile for using suspense
-      isEnabledLinebreaks: this.dummyIsEnabledLinebreaks,
+      isEnabledLinebreaks: false,
       isEnabledLinebreaksInComments: false,
       adminPreferredIndentSize: 4,
       isIndentSizeForced: false,

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

@@ -12,14 +12,12 @@ export default class AdminSlackIntegrationLegacyContainer extends Container {
     super();
 
     this.appContainer = appContainer;
-    this.dummyWebhookUrl = 0;
-    this.dummyWebhookUrlForError = 1;
 
     this.state = {
       isSlackbotConfigured: false,
       retrieveError: null,
       selectSlackOption: 'Incoming Webhooks',
-      webhookUrl: this.dummyWebhookUrl,
+      webhookUrl: '',
       isIncomingWebhookPrioritized: false,
       slackToken: '',
     };

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

@@ -135,6 +135,7 @@ export default class PageContainer extends Container {
 
   /**
    * initialize state for markdown data
+   * [Already SWRized]
    */
   initStateMarkdown() {
     let pageContent = '';

+ 0 - 73
packages/app/src/client/util/color-scheme.js

@@ -1,73 +0,0 @@
-const mediaQueryListForDarkMode = window.matchMedia('(prefers-color-scheme: dark)');
-
-function isUserPreferenceExists() {
-  return localStorage.preferDarkModeByUser != null;
-}
-
-function isPreferedDarkModeByUser() {
-  return localStorage.preferDarkModeByUser === 'true';
-}
-
-function isDarkMode() {
-  if (isUserPreferenceExists()) {
-    return isPreferedDarkModeByUser();
-  }
-  return mediaQueryListForDarkMode.matches;
-}
-
-/**
- * Apply color scheme as 'dark' attribute of <html></html>
- */
-function applyColorScheme() {
-  let isDarkMode = mediaQueryListForDarkMode.matches;
-  if (isUserPreferenceExists()) {
-    isDarkMode = isPreferedDarkModeByUser();
-  }
-
-  // switch to dark mode
-  if (isDarkMode) {
-    document.documentElement.removeAttribute('light');
-    document.documentElement.setAttribute('dark', 'true');
-  }
-  // switch to light mode
-  else {
-    document.documentElement.setAttribute('light', 'true');
-    document.documentElement.removeAttribute('dark');
-  }
-}
-
-/**
- * Remove color scheme preference
- */
-function removeUserPreference() {
-  if (isUserPreferenceExists()) {
-    delete localStorage.removeItem('preferDarkModeByUser');
-  }
-}
-
-/**
- * Set color scheme preference
- * @param {boolean} isDarkMode
- */
-function updateUserPreference(isDarkMode) {
-  // store settings to localStorage
-  localStorage.preferDarkModeByUser = isDarkMode;
-}
-
-/**
- * Set color scheme preference with OS settings
- */
-function updateUserPreferenceWithOsSettings() {
-  localStorage.preferDarkModeByUser = mediaQueryListForDarkMode.matches;
-}
-
-export {
-  mediaQueryListForDarkMode,
-  isUserPreferenceExists,
-  isPreferedDarkModeByUser,
-  isDarkMode,
-  applyColorScheme,
-  removeUserPreference,
-  updateUserPreference,
-  updateUserPreferenceWithOsSettings,
-};

+ 0 - 51
packages/app/src/components/Admin/App/AppSettingsPage.jsx

@@ -1,51 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import loggerFactory from '~/utils/logger';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-import { toastError } from '~/client/util/apiNotification';
-import { toArrayIfNot } from '~/utils/array-utils';
-import { withLoadingSppiner } from '../../SuspenseUtils';
-
-import AdminAppContainer from '~/client/services/AdminAppContainer';
-
-import AppSettingsPageContents from './AppSettingsPageContents';
-
-const logger = loggerFactory('growi:appSettings');
-
-let retrieveErrors = null;
-function AppSettingsPage(props) {
-  if (props.adminAppContainer.state.title === props.adminAppContainer.dummyTitle) {
-    throw (async() => {
-      try {
-        await props.adminAppContainer.retrieveAppSettingsData();
-      }
-      catch (err) {
-        const errs = toArrayIfNot(err);
-        toastError(errs);
-        logger.error(errs);
-        props.adminAppContainer.setState({
-          title: props.adminAppContainer.dummyTitleForError,
-        });
-        retrieveErrors = errs;
-      }
-    })();
-  }
-
-  if (props.adminAppContainer.state.title === props.adminAppContainer.dummyTitleForError) {
-    throw new Error(`${retrieveErrors.length} errors occured`);
-  }
-
-  return <AppSettingsPageContents />;
-}
-
-AppSettingsPage.propTypes = {
-  adminAppContainer: PropTypes.instanceOf(AdminAppContainer).isRequired,
-};
-
-/**
- * Wrapper component for using unstated
- */
-const AppSettingsPageWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(AppSettingsPage), [AdminAppContainer]);
-
-export default AppSettingsPageWithUnstatedContainer;

+ 22 - 1
packages/app/src/components/Admin/App/AppSettingsPageContents.tsx

@@ -1,8 +1,12 @@
-import React from 'react';
+import React, { useEffect } from 'react';
 
 import { useTranslation } from 'next-i18next';
 
 import AdminAppContainer from '~/client/services/AdminAppContainer';
+import { toastError } from '~/client/util/apiNotification';
+import { toArrayIfNot } from '~/utils/array-utils';
+import loggerFactory from '~/utils/logger';
+
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
@@ -14,6 +18,8 @@ import PluginSetting from './PluginSetting';
 import SiteUrlSetting from './SiteUrlSetting';
 import V5PageMigration from './V5PageMigration';
 
+const logger = loggerFactory('growi:appSettings');
+
 type Props = {
   adminAppContainer: AdminAppContainer,
 }
@@ -23,6 +29,21 @@ const AppSettingsPageContents = (props: Props) => {
   const { adminAppContainer } = props;
   const { isV5Compatible } = adminAppContainer.state;
 
+  useEffect(() => {
+    const fetchAppSettingsData = async() => {
+      await adminAppContainer.retrieveAppSettingsData();
+    };
+
+    try {
+      fetchAppSettingsData();
+    }
+    catch (err) {
+      const errs = toArrayIfNot(err);
+      toastError(errs);
+      logger.error(errs);
+    }
+  }, [adminAppContainer]);
+
   return (
     <div data-testid="admin-app-settings">
       {

+ 1 - 1
packages/app/src/components/Admin/App/FileUploadSetting.tsx

@@ -40,7 +40,7 @@ const FileUploadSetting = (props: Props) => {
         <br />
         <br />
         <span className="text-danger">
-          <i className="ti-unlink"></i>
+          <i className="ti ti-unlink"></i>
           {t('admin:app_setting.change_setting')}
         </span>
       </p>

+ 1 - 2
packages/app/src/components/Admin/App/SesSetting.tsx

@@ -3,7 +3,6 @@ import React from 'react';
 
 import AdminAppContainer from '~/client/services/AdminAppContainer';
 
-import { withLoadingSppiner } from '../../SuspenseUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
 type Props = {
@@ -57,6 +56,6 @@ const SmtpSetting = (props: Props) => {
 /**
  * Wrapper component for using unstated
  */
-const SmtpSettingWrapper = withUnstatedContainers(withLoadingSppiner(SmtpSetting), [AdminAppContainer]);
+const SmtpSettingWrapper = withUnstatedContainers(SmtpSetting, [AdminAppContainer]);
 
 export default SmtpSettingWrapper;

+ 1 - 2
packages/app/src/components/Admin/App/SmtpSetting.tsx

@@ -5,7 +5,6 @@ import { useTranslation } from 'next-i18next';
 
 import AdminAppContainer from '~/client/services/AdminAppContainer';
 
-import { withLoadingSppiner } from '../../SuspenseUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
 
@@ -82,5 +81,5 @@ const SmtpSetting = (props: Props) => {
 /**
  * Wrapper component for using unstated
  */
-const SmtpSettingWrapper = withUnstatedContainers(withLoadingSppiner(SmtpSetting), [AdminAppContainer]);
+const SmtpSettingWrapper = withUnstatedContainers(SmtpSetting, [AdminAppContainer]);
 export default SmtpSettingWrapper;

+ 0 - 56
packages/app/src/components/Admin/CustomCssEditor.jsx

@@ -1,56 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import { UnControlled as CodeMirror } from 'react-codemirror2';
-
-require('codemirror/addon/lint/css-lint');
-require('codemirror/addon/hint/css-hint');
-require('codemirror/addon/hint/show-hint');
-require('codemirror/addon/edit/matchbrackets');
-require('codemirror/addon/edit/closebrackets');
-require('codemirror/mode/css/css');
-require('~/client/util/codemirror/autorefresh.ext');
-
-require('jquery-ui/ui/widgets/resizable');
-
-export default class CustomCssEditor extends React.Component {
-
-  render() {
-
-    return (
-      <CodeMirror
-        value={this.props.value}
-        autoFocus
-        detach
-        options={{
-          mode: 'css',
-          lineNumbers: true,
-          tabSize: 2,
-          indentUnit: 2,
-          theme: 'eclipse',
-          autoRefresh: { force: true }, // force option is enabled by autorefresh.ext.js -- Yuki Takei
-          matchBrackets: true,
-          autoCloseBrackets: true,
-          extraKeys: { 'Ctrl-Space': 'autocomplete' },
-        }}
-        editorDidMount={(editor, next) => {
-          // resizable with jquery.ui
-          $(editor.getWrapperElement()).resizable({
-            resize() {
-              editor.setSize($(this).width(), $(this).height());
-            },
-          });
-        }}
-        onChange={(editor, data, value) => {
-          this.props.onChange(value);
-        }}
-      />
-    );
-  }
-
-}
-
-CustomCssEditor.propTypes = {
-  value: PropTypes.string.isRequired,
-  onChange: PropTypes.func.isRequired,
-};

+ 0 - 57
packages/app/src/components/Admin/CustomHeaderEditor.jsx

@@ -1,57 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import { UnControlled as CodeMirror } from 'react-codemirror2';
-
-require('codemirror/addon/hint/show-hint');
-require('codemirror/addon/edit/matchbrackets');
-require('codemirror/addon/edit/closebrackets');
-require('codemirror/mode/htmlmixed/htmlmixed');
-require('codemirror/addon/hint/html-hint');
-require('codemirror/addon/edit/closetag');
-require('~/client/util/codemirror/autorefresh.ext');
-
-require('jquery-ui/ui/widgets/resizable');
-
-export default class CustomHeaderEditor extends React.Component {
-
-  render() {
-
-    return (
-      <CodeMirror
-        value={this.props.value}
-        autoFocus
-        detach
-        options={{
-          mode: 'htmlmixed',
-          autoCloseTags: true,
-          lineNumbers: true,
-          tabSize: 2,
-          indentUnit: 2,
-          theme: 'eclipse',
-          autoRefresh: { force: true }, // force option is enabled by autorefresh.ext.js -- Yuki Takei
-          matchBrackets: true,
-          autoCloseBrackets: true,
-          extraKeys: { 'Ctrl-Space': 'autocomplete' },
-        }}
-        editorDidMount={(editor, next) => {
-          // resizable with jquery.ui
-          $(editor.getWrapperElement()).resizable({
-            resize() {
-              editor.setSize($(this).width(), $(this).height());
-            },
-          });
-        }}
-        onChange={(editor, data, value) => {
-          this.props.onChange(value);
-        }}
-      />
-    );
-  }
-
-}
-
-CustomHeaderEditor.propTypes = {
-  value: PropTypes.string.isRequired,
-  onChange: PropTypes.func.isRequired,
-};

+ 0 - 56
packages/app/src/components/Admin/CustomScriptEditor.jsx

@@ -1,56 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import { UnControlled as CodeMirror } from 'react-codemirror2';
-
-require('codemirror/addon/lint/javascript-lint');
-require('codemirror/addon/hint/javascript-hint');
-require('codemirror/addon/hint/show-hint');
-require('codemirror/addon/edit/matchbrackets');
-require('codemirror/addon/edit/closebrackets');
-require('codemirror/mode/javascript/javascript');
-require('~/client/util/codemirror/autorefresh.ext');
-
-require('jquery-ui/ui/widgets/resizable');
-
-export default class CustomScriptEditor extends React.Component {
-
-  render() {
-
-    return (
-      <CodeMirror
-        value={this.props.value}
-        autoFocus
-        detach
-        options={{
-          mode: 'javascript',
-          lineNumbers: true,
-          tabSize: 2,
-          indentUnit: 2,
-          theme: 'eclipse',
-          autoRefresh: { force: true }, // force option is enabled by autorefresh.ext.js -- Yuki Takei
-          matchBrackets: true,
-          autoCloseBrackets: true,
-          extraKeys: { 'Ctrl-Space': 'autocomplete' },
-        }}
-        editorDidMount={(editor, next) => {
-          // resizable with jquery.ui
-          $(editor.getWrapperElement()).resizable({
-            resize() {
-              editor.setSize($(this).width(), $(this).height());
-            },
-          });
-        }}
-        onChange={(editor, data, value) => {
-          this.props.onChange(value);
-        }}
-      />
-    );
-  }
-
-}
-
-CustomScriptEditor.propTypes = {
-  value: PropTypes.string.isRequired,
-  onChange: PropTypes.func.isRequired,
-};

+ 25 - 26
packages/app/src/components/Admin/Customize/Customize.jsx

@@ -1,15 +1,13 @@
 
-import React, { Fragment } from 'react';
+import React, { useEffect } from 'react';
 
 import PropTypes from 'prop-types';
 
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { toastError } from '~/client/util/apiNotification';
 import { toArrayIfNot } from '~/utils/array-utils';
 import loggerFactory from '~/utils/logger';
 
-import { withLoadingSppiner } from '../../SuspenseUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
 import CustomizeCssSetting from './CustomizeCssSetting';
@@ -17,6 +15,7 @@ import CustomizeFunctionSetting from './CustomizeFunctionSetting';
 import CustomizeHeaderSetting from './CustomizeHeaderSetting';
 import CustomizeHighlightSetting from './CustomizeHighlightSetting';
 import CustomizeLayoutSetting from './CustomizeLayoutSetting';
+import CustomizeLogoSetting from './CustomizeLogoSetting';
 import CustomizeScriptSetting from './CustomizeScriptSetting';
 import CustomizeSidebarSetting from './CustomizeSidebarSetting';
 import CustomizeThemeSetting from './CustomizeThemeSetting';
@@ -24,39 +23,37 @@ import CustomizeTitle from './CustomizeTitle';
 
 const logger = loggerFactory('growi:services:AdminCustomizePage');
 
-let retrieveErrors = null;
 function Customize(props) {
-  const { appContainer, adminCustomizeContainer } = props;
+  const { adminCustomizeContainer } = props;
 
-  if (adminCustomizeContainer.state.currentTheme === adminCustomizeContainer.dummyCurrentTheme) {
-    throw (async() => {
-      try {
-        await adminCustomizeContainer.retrieveCustomizeData();
-      }
-      catch (err) {
-        const errs = toArrayIfNot(err);
-        toastError(errs);
-        logger.error(errs);
-        retrieveErrors = errs;
-        adminCustomizeContainer.setState({ currentTheme: adminCustomizeContainer.dummyCurrentThemeForError });
-      }
-    })();
-  }
+  useEffect(() => {
+    async function fetchCustomizeSettingsData() {
+      await adminCustomizeContainer.retrieveCustomizeData();
+    }
+
+    try {
+      fetchCustomizeSettingsData();
+    }
+    catch (err) {
+      const errs = toArrayIfNot(err);
+      toastError(errs);
+      logger.error(errs);
+    }
+  }, [adminCustomizeContainer]);
 
-  if (adminCustomizeContainer.state.currentTheme === adminCustomizeContainer.dummyCurrentThemeForError) {
-    throw new Error(`${retrieveErrors.length} errors occured`);
-  }
 
   return (
     <div data-testid="admin-customize">
       <div className="mb-5">
-        <CustomizeLayoutSetting appContainer={appContainer} />
+        <CustomizeLayoutSetting />
       </div>
       <div className="mb-5">
         <CustomizeThemeSetting />
       </div>
       <div className="mb-5">
-        <CustomizeSidebarSetting />
+        {/* TODO: [resolve browser err] A component is changing an uncontrolled input to be controlled. by https://redmine.weseek.co.jp/issues/101155
+          <CustomizeSidebarSetting />
+        */}
       </div>
       <div className="mb-5">
         <CustomizeFunctionSetting />
@@ -76,14 +73,16 @@ function Customize(props) {
       <div className="mb-5">
         <CustomizeScriptSetting />
       </div>
+      <div className="mb-5">
+        <CustomizeLogoSetting />
+      </div>
     </div>
   );
 }
 
-const CustomizePageWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(Customize), [AppContainer, AdminCustomizeContainer]);
+const CustomizePageWithUnstatedContainer = withUnstatedContainers(Customize, [AdminCustomizeContainer]);
 
 Customize.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
 };
 

+ 5 - 6
packages/app/src/components/Admin/Customize/CustomizeCssSetting.tsx

@@ -4,15 +4,12 @@ import { useTranslation } from 'next-i18next';
 import { Card, CardBody } from 'reactstrap';
 
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-import CustomCssEditor from '../CustomCssEditor';
 
 type Props = {
-  appContainer: AppContainer,
   adminCustomizeContainer: AdminCustomizeContainer
 }
 
@@ -45,9 +42,11 @@ const CustomizeCssSetting = (props: Props): JSX.Element => {
           </Card>
 
           <div className="form-group">
-            <CustomCssEditor
+            <textarea
+              className="form-control"
+              name="customizeCss"
               value={adminCustomizeContainer.state.currentCustomizeCss || ''}
-              onChange={(inputValue) => { adminCustomizeContainer.changeCustomizeCss(inputValue) }}
+              onChange={(e) => { adminCustomizeContainer.changeCustomizeCss(e.target.value) }}
             />
             <p className="form-text text-muted text-right">
               <i className="fa fa-fw fa-keyboard-o" aria-hidden="true" />
@@ -63,6 +62,6 @@ const CustomizeCssSetting = (props: Props): JSX.Element => {
 
 };
 
-const CustomizeCssSettingWrapper = withUnstatedContainers(CustomizeCssSetting, [AppContainer, AdminCustomizeContainer]);
+const CustomizeCssSettingWrapper = withUnstatedContainers(CustomizeCssSetting, [AdminCustomizeContainer]);
 
 export default CustomizeCssSettingWrapper;

+ 1 - 3
packages/app/src/components/Admin/Customize/CustomizeFunctionSetting.tsx

@@ -4,7 +4,6 @@ import { useTranslation } from 'next-i18next';
 import { Card, CardBody } from 'reactstrap';
 
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -14,7 +13,6 @@ import CustomizeFunctionOption from './CustomizeFunctionOption';
 import PagingSizeUncontrolledDropdown from './PagingSizeUncontrolledDropdown';
 
 type Props = {
-  appContainer: AppContainer,
   adminCustomizeContainer: AdminCustomizeContainer
 }
 
@@ -158,6 +156,6 @@ const CustomizeFunctionSetting = (props: Props): JSX.Element => {
 
 };
 
-const CustomizeFunctionSettingWrapper = withUnstatedContainers(CustomizeFunctionSetting, [AppContainer, AdminCustomizeContainer]);
+const CustomizeFunctionSettingWrapper = withUnstatedContainers(CustomizeFunctionSetting, [AdminCustomizeContainer]);
 
 export default CustomizeFunctionSettingWrapper;

+ 4 - 3
packages/app/src/components/Admin/Customize/CustomizeHeaderSetting.tsx

@@ -8,7 +8,6 @@ import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-import CustomHeaderEditor from '../CustomHeaderEditor';
 
 type Props = {
   adminCustomizeContainer: AdminCustomizeContainer
@@ -54,9 +53,11 @@ const CustomizeHeaderSetting = (props: Props): JSX.Element => {
           </div>
 
           <div className="form-group">
-            <CustomHeaderEditor
+            <textarea
+              className="form-control"
+              name="customizeHeader"
               value={adminCustomizeContainer.state.currentCustomizeHeader || ''}
-              onChange={(inputValue) => { adminCustomizeContainer.changeCustomizeHeader(inputValue) }}
+              onChange={(e) => { adminCustomizeContainer.changeCustomizeHeader(e.target.value) }}
             />
             <p className="form-text text-muted text-right">
               <i className="fa fa-fw fa-keyboard-o" aria-hidden="true"></i>

+ 5 - 6
packages/app/src/components/Admin/Customize/CustomizeLayoutSetting.tsx

@@ -4,14 +4,13 @@ import { useTranslation } from 'next-i18next';
 
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
-import { isDarkMode as isDarkModeByUtil } from '~/client/util/color-scheme';
-
-const isDarkMode = isDarkModeByUtil();
-const colorText = isDarkMode ? 'dark' : 'light';
+import { useNextThemes } from '~/stores/use-next-themes';
 
 const CustomizeLayoutSetting = (): JSX.Element => {
   const { t } = useTranslation();
 
+  const { resolvedTheme } = useNextThemes();
+
   const [isContainerFluid, setIsContainerFluid] = useState(false);
   const [retrieveError, setRetrieveError] = useState();
 
@@ -54,7 +53,7 @@ const CustomizeLayoutSetting = (): JSX.Element => {
                 onClick={() => setIsContainerFluid(false)}
                 role="button"
               >
-                <img src={`/images/customize-settings/default-${colorText}.svg`} />
+                <img src={`/images/customize-settings/default-${resolvedTheme}.svg`} />
                 <div className="card-body text-center">
                   {t('admin:customize_setting.layout_options.default')}
                 </div>
@@ -64,7 +63,7 @@ const CustomizeLayoutSetting = (): JSX.Element => {
                 onClick={() => setIsContainerFluid(true)}
                 role="button"
               >
-                <img src={`/images/customize-settings/fluid-${colorText}.svg`} />
+                <img src={`/images/customize-settings/fluid-${resolvedTheme}.svg`} />
                 <div className="card-body  text-center">
                   {t('admin:customize_setting.layout_options.expanded')}
                 </div>

+ 185 - 0
packages/app/src/components/Admin/Customize/CustomizeLogoSetting.tsx

@@ -0,0 +1,185 @@
+import React, { useCallback, useEffect, useState } from 'react';
+
+import { useTranslation } from 'react-i18next';
+
+import { toastError, toastSuccess } from '~/client/util/apiNotification';
+import {
+  apiv3Delete, apiv3Get, apiv3PostForm, apiv3Put,
+} from '~/client/util/apiv3-client';
+import ImageCropModal from '~/components/Common/ImageCropModal';
+
+import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
+
+const DEFAULT_LOGO = '/images/logo.svg';
+
+const CustomizeLogoSetting = (): JSX.Element => {
+
+  const { t } = useTranslation();
+
+  const [uploadLogoSrc, setUploadLogoSrc] = useState<ArrayBuffer | string | null>(null);
+  const [isImageCropModalShow, setIsImageCropModalShow] = useState<boolean>(false);
+  const [isDefaultLogo, setIsDefaultLogo] = useState<boolean>(true);
+  const [retrieveError, setRetrieveError] = useState<string | null>(null);
+  const [customizedLogoSrc, setCustomizedLogoSrc] = useState< string | null >(null);
+
+  const retrieveData = useCallback(async() => {
+    try {
+      const response = await apiv3Get('/customize-setting/customize-logo');
+      const { isDefaultLogo: _isDefaultLogo, customizedLogoSrc } = response.data;
+      const isDefaultLogo = _isDefaultLogo ?? true;
+
+      setIsDefaultLogo(isDefaultLogo);
+      setCustomizedLogoSrc(customizedLogoSrc);
+    }
+    catch (err) {
+      setRetrieveError(err);
+      throw new Error('Failed to fetch data');
+    }
+  }, []);
+
+  useEffect(() => {
+    retrieveData();
+  }, [retrieveData]);
+
+  const onSelectFile = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
+    if (e.target.files != null && e.target.files.length > 0) {
+      const reader = new FileReader();
+      reader.addEventListener('load', () => setUploadLogoSrc(reader.result));
+      reader.readAsDataURL(e.target.files[0]);
+      setIsImageCropModalShow(true);
+    }
+  }, []);
+
+  const onClickSubmit = useCallback(async() => {
+    try {
+      const response = await apiv3Put('/customize-setting/customize-logo', {
+        isDefaultLogo,
+        customizedLogoSrc,
+      });
+      const { customizedParams } = response.data;
+      setIsDefaultLogo(customizedParams.isDefaultLogo);
+      setCustomizedLogoSrc(customizedParams.customizedLogoSrc);
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.custom_logo') }));
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }, [t, isDefaultLogo, customizedLogoSrc]);
+
+
+  const onClickDeleteBtn = useCallback(async() => {
+    try {
+      await apiv3Delete('/customize-setting/delete-brand-logo');
+      setCustomizedLogoSrc(null);
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.current_logo') }));
+    }
+    catch (err) {
+      toastError(err);
+      setRetrieveError(err);
+      throw new Error('Failed to delete logo');
+    }
+  }, [t]);
+
+  const onCropCompleted = useCallback(async(croppedImage) => {
+    try {
+      const formData = new FormData();
+      formData.append('file', croppedImage);
+      const { data } = await apiv3PostForm('/customize-setting/upload-brand-logo', formData);
+      setCustomizedLogoSrc(data.attachment.filePathProxied);
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.current_logo') }));
+    }
+    catch (err) {
+      toastError(err);
+      setRetrieveError(err);
+      throw new Error('Failed to upload brand logo');
+    }
+    setIsImageCropModalShow(false);
+  }, [t]);
+
+
+  return (
+    <React.Fragment>
+      <div className="row">
+        <div className="col-12">
+          <div className="mb-5">
+            <h2 className="border-bottom my-4 admin-setting-header">{t('admin:customize_setting.custom_logo')}</h2>
+            <div className="row">
+              <div className="col-md-6 col-12 mb-3 mb-md-0">
+                <h4>
+                  <div className="custom-control custom-radio radio-primary">
+                    <input
+                      type="radio"
+                      id="radioDefaultLogo"
+                      className="custom-control-input"
+                      form="formImageType"
+                      name="imagetypeForm[isDefaultLogo]"
+                      checked={isDefaultLogo}
+                      onChange={() => { setIsDefaultLogo(true) }}
+                    />
+                    <label className="custom-control-label" htmlFor="radioDefaultLogo">
+                      {t('admin:customize_setting.default_logo')}
+                    </label>
+                  </div>
+                </h4>
+                <img src={DEFAULT_LOGO} width="64" />
+              </div>
+              <div className="col-md-6 col-12">
+                <h4>
+                  <div className="custom-control custom-radio radio-primary">
+                    <input
+                      type="radio"
+                      id="radioUploadLogo"
+                      className="custom-control-input"
+                      form="formImageType"
+                      name="imagetypeForm[isDefaultLogo]"
+                      checked={!isDefaultLogo}
+                      onChange={() => { setIsDefaultLogo(false) }}
+                    />
+                    <label className="custom-control-label" htmlFor="radioUploadLogo">
+                      { t('admin:customize_setting.upload_logo') }
+                    </label>
+                  </div>
+                </h4>
+                <div className="row mb-3">
+                  <label className="col-sm-4 col-12 col-form-label text-left">
+                    { t('admin:customize_setting.current_logo') }
+                  </label>
+                  <div className="col-sm-8 col-12">
+                    <p><img src={customizedLogoSrc || DEFAULT_LOGO} className="picture picture-lg " id="settingBrandLogo" width="64" /></p>
+                    {(customizedLogoSrc != null) && (
+                      <button type="button" className="btn btn-danger" onClick={onClickDeleteBtn}>
+                        { t('admin:customize_setting.delete_logo') }
+                      </button>
+                    )}
+                  </div>
+                </div>
+                <div className="row">
+                  <label className="col-sm-4 col-12 col-form-label text-left">
+                    { t('admin:customize_setting.upload_new_logo') }
+                  </label>
+                  <div className="col-sm-8 col-12">
+                    <input type="file" onChange={onSelectFile} name="brandLogo" accept="image/*" />
+                  </div>
+                </div>
+              </div>
+            </div>
+            <AdminUpdateButtonRow onClick={onClickSubmit} disabled={retrieveError != null} />
+          </div>
+        </div>
+      </div>
+
+      <ImageCropModal
+        isShow={isImageCropModalShow}
+        src={uploadLogoSrc}
+        onModalClose={() => setIsImageCropModalShow(false)}
+        onCropCompleted={onCropCompleted}
+        isCircular={false}
+      />
+    </React.Fragment>
+  );
+
+
+};
+
+
+export default CustomizeLogoSetting;

+ 4 - 3
packages/app/src/components/Admin/Customize/CustomizeScriptSetting.tsx

@@ -8,7 +8,6 @@ import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-import CustomScriptEditor from '../CustomScriptEditor';
 
 type Props = {
   adminCustomizeContainer: AdminCustomizeContainer
@@ -84,9 +83,11 @@ const CustomizeScriptSetting = (props: Props): JSX.Element => {
           </div>
 
           <div className="form-group">
-            <CustomScriptEditor
+            <textarea
+              className="form-control"
+              name="customizeScript"
               value={adminCustomizeContainer.state.currentCustomizeScript || ''}
-              onChange={(inputValue) => { adminCustomizeContainer.changeCustomizeScript(inputValue) }}
+              onChange={(e) => { adminCustomizeContainer.changeCustomizeScript(e.target.value) }}
             />
             <p className="form-text text-muted text-right">
               <i className="fa fa-fw fa-keyboard-o" aria-hidden="true" />

+ 4 - 5
packages/app/src/components/Admin/Customize/CustomizeSidebarSetting.tsx

@@ -4,8 +4,8 @@ import { useTranslation } from 'next-i18next';
 import { Card, CardBody } from 'reactstrap';
 
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
-import { isDarkMode as isDarkModeByUtil } from '~/client/util/color-scheme';
 import { useSWRxSidebarConfig } from '~/stores/ui';
+import { useNextThemes } from '~/stores/use-next-themes';
 
 const CustomizeSidebarsetting = (): JSX.Element => {
   const { t } = useTranslation();
@@ -13,10 +13,9 @@ const CustomizeSidebarsetting = (): JSX.Element => {
     update, isSidebarDrawerMode, isSidebarClosedAtDockMode, setIsSidebarDrawerMode, setIsSidebarClosedAtDockMode,
   } = useSWRxSidebarConfig();
 
-  const isDarkMode = isDarkModeByUtil();
-  const colorText = isDarkMode ? 'dark' : 'light';
-  const drawerIconFileName = `/images/customize-settings/drawer-${colorText}.svg`;
-  const dockIconFileName = `/images/customize-settings/dock-${colorText}.svg`;
+  const { resolvedTheme } = useNextThemes();
+  const drawerIconFileName = `/images/customize-settings/drawer-${resolvedTheme}.svg`;
+  const dockIconFileName = `/images/customize-settings/dock-${resolvedTheme}.svg`;
 
   const onClickSubmit = useCallback(async() => {
     try {

+ 21 - 18
packages/app/src/components/Admin/Customize/CustomizeThemeOptions.jsx

@@ -1,10 +1,12 @@
 import React from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
+import { GrowiThemes } from '~/interfaces/theme';
+import { useGrowiTheme } from '~/stores/context';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
@@ -12,37 +14,37 @@ import ThemeColorBox from './ThemeColorBox';
 
 /* eslint-disable no-multi-spaces */
 const lightNDarkTheme = [{
-  name: 'default',    bg: '#ffffff', topbar: '#2a2929', sidebar: '#122c55', theme: '#209fd8',
+  name: GrowiThemes.DEFAULT,      bg: '#ffffff', topbar: '#2a2929', sidebar: '#122c55', theme: '#209fd8',
 }, {
-  name: 'mono-blue',  bg: '#F7FBFD', topbar: '#2a2929', sidebar: '#00587A', theme: '#00587A',
+  name: GrowiThemes.MONO_BLUE,    bg: '#F7FBFD', topbar: '#2a2929', sidebar: '#00587A', theme: '#00587A',
 }, {
-  name: 'hufflepuff',  bg: '#EFE2CF', topbar: '#2a2929', sidebar: '#EAAB20', theme: '#993439',
+  name: GrowiThemes.HUFFLEPUFF,   bg: '#EFE2CF', topbar: '#2a2929', sidebar: '#EAAB20', theme: '#993439',
 }, {
-  name: 'fire-red',  bg: '#FDFDFD', topbar: '#2c2c2c', sidebar: '#BFBFBF', theme: '#EA5532',
+  name: GrowiThemes.FIRE_RED,     bg: '#FDFDFD', topbar: '#2c2c2c', sidebar: '#BFBFBF', theme: '#EA5532',
 }, {
-  name: 'jade-green',  bg: '#FDFDFD', topbar: '#2c2c2c', sidebar: '#BFBFBF', theme: '#38B48B',
+  name: GrowiThemes.JADE_GREEN,   bg: '#FDFDFD', topbar: '#2c2c2c', sidebar: '#BFBFBF', theme: '#38B48B',
 }];
 
 const uniqueTheme = [{
-  name: 'nature',     bg: '#f9fff3', topbar: '#234136', sidebar: '#118050', theme: '#460039',
+  name: GrowiThemes.NATURE,       bg: '#f9fff3', topbar: '#234136', sidebar: '#118050', theme: '#460039',
 }, {
-  name: 'wood',       bg: '#fffefb', topbar: '#2a2929', sidebar: '#aaa45f', theme: '#aaa45f',
+  name: GrowiThemes.WOOD,         bg: '#fffefb', topbar: '#2a2929', sidebar: '#aaa45f', theme: '#aaa45f',
 }, {
-  name: 'island',     bg: '#cef2ef', topbar: '#2a2929', sidebar: '#0c2a44', theme: 'rgba(183, 226, 219, 1)',
+  name: GrowiThemes.ISLAND,       bg: '#cef2ef', topbar: '#2a2929', sidebar: '#0c2a44', theme: 'rgba(183, 226, 219, 1)',
 }, {
-  name: 'christmas',  bg: '#fffefb', topbar: '#b3000c', sidebar: '#30882c', theme: '#d3c665',
+  name: GrowiThemes.CHRISTMAS,    bg: '#fffefb', topbar: '#b3000c', sidebar: '#30882c', theme: '#d3c665',
 }, {
-  name: 'antarctic',  bg: '#ffffff', topbar: '#2a2929', sidebar: '#000080', theme: '#fa9913',
+  name: GrowiThemes.ANTARCTIC,    bg: '#ffffff', topbar: '#2a2929', sidebar: '#000080', theme: '#fa9913',
 }, {
-  name: 'spring',     bg: '#ffffff', topbar: '#d3687c', sidebar: '#ffb8c6', theme: '#67a856',
+  name: GrowiThemes.SPRING,       bg: '#ffffff', topbar: '#d3687c', sidebar: '#ffb8c6', theme: '#67a856',
 }, {
-  name: 'future',     bg: '#16282d', topbar: '#2a2929', sidebar: '#00b5b7', theme: '#00b5b7',
+  name: GrowiThemes.FUTURE,       bg: '#16282d', topbar: '#2a2929', sidebar: '#00b5b7', theme: '#00b5b7',
 }, {
-  name: 'halloween',  bg: '#030003', topbar: '#aa4a04', sidebar: '#162b33', theme: '#e9af2b',
+  name: GrowiThemes.HALLOWEEN,    bg: '#030003', topbar: '#aa4a04', sidebar: '#162b33', theme: '#e9af2b',
 }, {
-  name: 'kibela',  bg: '#f4f5f6', topbar: '#1256a3', sidebar: '#5882fa', theme: '#b5cbf79c',
+  name: GrowiThemes.KIBELA,       bg: '#f4f5f6', topbar: '#1256a3', sidebar: '#5882fa', theme: '#b5cbf79c',
 }, {
-  name: 'blackboard',  bg: '#223729', topbar: '#563E23', sidebar: '#7B5932', theme: '#DA8506',
+  name: GrowiThemes.BLACKBOARD,   bg: '#223729', topbar: '#563E23', sidebar: '#7B5932', theme: '#DA8506',
 }];
 
 
@@ -50,6 +52,7 @@ const CustomizeThemeOptions = (props) => {
 
   const { adminCustomizeContainer } = props;
   const { t } = useTranslation();
+  const { mutate: mutateGrowiTheme } = useGrowiTheme();
   const { currentLayout, currentTheme } = adminCustomizeContainer.state;
 
   return (
@@ -63,7 +66,7 @@ const CustomizeThemeOptions = (props) => {
               <ThemeColorBox
                 key={theme.name}
                 isSelected={currentTheme === theme.name}
-                onSelected={() => adminCustomizeContainer.switchThemeType(theme.name)}
+                onSelected={() => mutateGrowiTheme(theme.name)}
                 {...theme}
               />
             );
@@ -79,7 +82,7 @@ const CustomizeThemeOptions = (props) => {
               <ThemeColorBox
                 key={theme.name}
                 isSelected={currentTheme === theme.name}
-                onSelected={() => adminCustomizeContainer.switchThemeType(theme.name)}
+                onSelected={() => mutateGrowiTheme(theme.name)}
                 {...theme}
               />
             );

+ 2 - 13
packages/app/src/components/Admin/Customize/CustomizeThemeSetting.tsx

@@ -19,7 +19,7 @@ const CustomizeThemeSetting = (props: Props): JSX.Element => {
   const { adminCustomizeContainer } = props;
   const { t } = useTranslation();
 
-  const onClickSubmit = useCallback(async() => {
+  const submitHandler = useCallback(async() => {
     try {
       await adminCustomizeContainer.updateCustomizeTheme();
       toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.theme') }));
@@ -29,24 +29,13 @@ const CustomizeThemeSetting = (props: Props): JSX.Element => {
     }
   }, [t, adminCustomizeContainer]);
 
-  const renderDevAlert = useCallback(() => {
-    if (process.env.NODE_ENV === 'development') {
-      return (
-        <div className="alert alert-warning">
-          <strong>DEBUG MESSAGE:</strong> Live preview for theme is disabled in development mode.
-        </div>
-      );
-    }
-  }, []);
-
   return (
     <React.Fragment>
       <div className="row">
         <div className="col-12">
           <h2 className="admin-setting-header">{t('admin:customize_setting.theme')}</h2>
-          {renderDevAlert()}
           <CustomizeThemeOptions />
-          <AdminUpdateButtonRow onClick={onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
+          <AdminUpdateButtonRow onClick={submitHandler} disabled={adminCustomizeContainer.state.retrieveError != null} />
         </div>
       </div>
     </React.Fragment>

+ 6 - 6
packages/app/src/components/Admin/Customize/CustomizeTitle.jsx

@@ -1,15 +1,16 @@
 /* eslint-disable max-len */
 import React from 'react';
-import PropTypes from 'prop-types';
+
 import { withTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 import { Card, CardBody } from 'reactstrap';
 
-import AppContainer from '~/client/services/AppContainer';
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
-import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
+import { withUnstatedContainers } from '../../UnstatedUtils';
+import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
+
 class CustomizeTitle extends React.Component {
 
   constructor(props) {
@@ -85,11 +86,10 @@ class CustomizeTitle extends React.Component {
 
 }
 
-const CustomizeTitleWrapper = withUnstatedContainers(CustomizeTitle, [AppContainer, AdminCustomizeContainer]);
+const CustomizeTitleWrapper = withUnstatedContainers(CustomizeTitle, [AdminCustomizeContainer]);
 
 CustomizeTitle.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
 };
 

+ 1 - 0
packages/app/src/components/Admin/Customize/ThemeColorBox.jsx

@@ -1,4 +1,5 @@
 import React from 'react';
+
 import PropTypes from 'prop-types';
 
 

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

@@ -1,245 +0,0 @@
-import React from 'react';
-
-import PropTypes from 'prop-types';
-import { useTranslation } from 'next-i18next';
-
-import AdminSocketIoContainer from '~/client/services/AdminSocketIoContainer';
-import AppContainer from '~/client/services/AppContainer';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
-import { apiv3Get, apiv3Post, apiv3Put } from '~/client/util/apiv3-client';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-
-import NormalizeIndicesControls from './NormalizeIndicesControls';
-import RebuildIndexControls from './RebuildIndexControls';
-import ReconnectControls from './ReconnectControls';
-import StatusTable from './StatusTable';
-
-class ElasticsearchManagement extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      isInitialized: false,
-
-      isConnected: false,
-      isConfigured: false,
-      isReconnectingProcessing: false,
-      isRebuildingProcessing: false,
-      isRebuildingCompleted: false,
-
-      isNormalized: null,
-      indicesData: null,
-      aliasesData: null,
-    };
-
-    this.reconnect = this.reconnect.bind(this);
-    this.normalizeIndices = this.normalizeIndices.bind(this);
-    this.rebuildIndices = this.rebuildIndices.bind(this);
-  }
-
-  async UNSAFE_UNSAFE_componentWillMount() {
-    this.retrieveIndicesStatus();
-  }
-
-  componentDidMount() {
-    this.initWebSockets();
-  }
-
-  initWebSockets() {
-    const socket = this.props.adminSocketIoContainer.getSocket();
-
-    socket.on('addPageProgress', (data) => {
-      this.setState({
-        isRebuildingProcessing: true,
-      });
-    });
-
-    socket.on('finishAddPage', async(data) => {
-      await this.retrieveIndicesStatus();
-      this.setState({
-        isRebuildingProcessing: false,
-        isRebuildingCompleted: true,
-      });
-    });
-
-    socket.on('rebuildingFailed', (data) => {
-      toastError(new Error(data.error), 'Rebuilding Index has failed.');
-    });
-  }
-
-  async retrieveIndicesStatus() {
-    const { appContainer } = this.props;
-
-    try {
-      const { data } = await apiv3Get('/search/indices');
-      const { info } = data;
-
-      this.setState({
-        isConnected: true,
-        isConfigured: true,
-
-        indicesData: info.indices,
-        aliasesData: info.aliases,
-        isNormalized: info.isNormalized,
-      });
-    }
-    catch (errors) {
-      this.setState({ isConnected: false });
-
-      // evaluate whether configured or not
-      for (const error of errors) {
-        if (error.code === 'search-service-unconfigured') {
-          this.setState({ isConfigured: false });
-        }
-      }
-
-      toastError(errors);
-    }
-    finally {
-      this.setState({ isInitialized: true });
-    }
-  }
-
-  async reconnect() {
-    const { appContainer } = this.props;
-
-    this.setState({ isReconnectingProcessing: true });
-
-    try {
-      await apiv3Post('/search/connection');
-    }
-    catch (e) {
-      toastError(e);
-      return;
-    }
-
-    // reload
-    window.location.reload();
-  }
-
-  async normalizeIndices() {
-    const { appContainer } = this.props;
-
-    try {
-      await apiv3Put('/search/indices', { operation: 'normalize' });
-    }
-    catch (e) {
-      toastError(e);
-    }
-
-    await this.retrieveIndicesStatus();
-
-    toastSuccess('Normalizing has succeeded');
-  }
-
-  async rebuildIndices() {
-    const { appContainer } = this.props;
-
-    this.setState({ isRebuildingProcessing: true });
-
-    try {
-      await apiv3Put('/search/indices', { operation: 'rebuild' });
-      toastSuccess('Rebuilding is requested');
-    }
-    catch (e) {
-      toastError(e);
-    }
-
-    await this.retrieveIndicesStatus();
-  }
-
-  render() {
-    const { t, appContainer } = this.props;
-    const {
-      isInitialized,
-      isConnected, isConfigured, isReconnectingProcessing, isRebuildingProcessing, isRebuildingCompleted,
-      isNormalized, indicesData, aliasesData,
-    } = this.state;
-
-    const isErrorOccuredOnSearchService = !appContainer.config.isSearchServiceReachable;
-
-    const isReconnectBtnEnabled = !isReconnectingProcessing && (!isInitialized || !isConnected || isErrorOccuredOnSearchService);
-
-    return (
-      <>
-        <div className="row">
-          <div className="col-md-12">
-            <StatusTable
-              isInitialized={isInitialized}
-              isErrorOccuredOnSearchService={isErrorOccuredOnSearchService}
-              isConnected={isConnected}
-              isConfigured={isConfigured}
-              isNormalized={isNormalized}
-              indicesData={indicesData}
-              aliasesData={aliasesData}
-            />
-          </div>
-        </div>
-
-        <hr />
-
-        {/* Controls */}
-        <div className="row">
-          <label className="col-md-3 col-form-label text-left text-md-right">{ t('full_text_search_management.reconnect') }</label>
-          <div className="col-md-6">
-            <ReconnectControls
-              isEnabled={isReconnectBtnEnabled}
-              isProcessing={isReconnectingProcessing}
-              onReconnectingRequested={this.reconnect}
-            />
-          </div>
-        </div>
-
-        <hr />
-
-        <div className="row">
-          <label className="col-md-3 col-form-label text-left text-md-right">{ t('full_text_search_management.normalize') }</label>
-          <div className="col-md-6">
-            <NormalizeIndicesControls
-              isRebuildingProcessing={isRebuildingProcessing}
-              isRebuildingCompleted={isRebuildingCompleted}
-              isNormalized={isNormalized}
-              onNormalizingRequested={this.normalizeIndices}
-            />
-          </div>
-        </div>
-
-        <hr />
-
-        <div className="row">
-          <label className="col-md-3 col-form-label text-left text-md-right">{ t('full_text_search_management.rebuild') }</label>
-          <div className="col-md-6">
-            <RebuildIndexControls
-              isRebuildingProcessing={isRebuildingProcessing}
-              isRebuildingCompleted={isRebuildingCompleted}
-              isNormalized={isNormalized}
-              onRebuildingRequested={this.rebuildIndices}
-            />
-          </div>
-        </div>
-
-      </>
-    );
-  }
-
-}
-
-const ElasticsearchManagementWrapperFC = (props) => {
-  const { t } = useTranslation();
-  return <ElasticsearchManagement t={t} {...props} />;
-};
-
-/**
- * Wrapper component for using unstated
- */
-const ElasticsearchManagementWrapper = withUnstatedContainers(ElasticsearchManagementWrapperFC, [AppContainer, AdminSocketIoContainer]);
-
-ElasticsearchManagement.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminSocketIoContainer: PropTypes.instanceOf(AdminSocketIoContainer).isRequired,
-};
-
-export default ElasticsearchManagementWrapper;

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

@@ -0,0 +1,212 @@
+import React, { useEffect, useState, useCallback } from 'react';
+
+import { useTranslation } from 'next-i18next';
+
+
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { apiv3Get, apiv3Post, apiv3Put } from '~/client/util/apiv3-client';
+import { SocketEventName } from '~/interfaces/websocket';
+import { useIsSearchServiceReachable } from '~/stores/context';
+import { useAdminSocket } from '~/stores/socket-io';
+
+import NormalizeIndicesControls from './NormalizeIndicesControls';
+import RebuildIndexControls from './RebuildIndexControls';
+import ReconnectControls from './ReconnectControls';
+import StatusTable from './StatusTable';
+
+const ElasticsearchManagement = () => {
+  const { t } = useTranslation();
+  const { data: isSearchServiceReachable } = useIsSearchServiceReachable();
+  const { data: socket } = useAdminSocket();
+
+  const [isInitialized, setIsInitialized] = useState(false);
+
+  const [isConnected, setIsConnected] = useState(false);
+  const [isConfigured, setIsConfigured] = useState(false);
+  const [isReconnectingProcessing, setIsReconnectingProcessing] = useState(false);
+  const [isRebuildingProcessing, setIsRebuildingProcessing] = useState(false);
+  const [isRebuildingCompleted, setIsRebuildingCompleted] = useState(false);
+
+  const [isNormalized, setIsNormalized] = useState(false);
+  const [indicesData, setIndicesData] = useState(null);
+  const [aliasesData, setAliasesData] = useState(null);
+
+
+  const retrieveIndicesStatus = useCallback(async() => {
+    try {
+      const { data } = await apiv3Get('/search/indices');
+      const { info } = data;
+
+      setIsConnected(true);
+      setIsConfigured(true);
+
+      setIndicesData(info.indices);
+      setAliasesData(info.aliases);
+      setIsNormalized(info.isNormalized);
+    }
+    catch (errors) {
+      setIsConnected(false);
+
+      // evaluate whether configured or not
+      for (const error of errors) {
+        if (error.code === 'search-service-unconfigured') {
+          setIsConfigured(false);
+        }
+      }
+
+      toastError(errors);
+    }
+    finally {
+      setIsInitialized(true);
+    }
+  }, []);
+
+  useEffect(() => {
+    const fetchIndicesStatusData = async() => {
+      await retrieveIndicesStatus();
+    };
+    fetchIndicesStatusData();
+  }, [retrieveIndicesStatus]);
+
+
+  useEffect(() => {
+    if (socket == null) {
+      return;
+    }
+    socket.on(SocketEventName.AddPageProgress, (data) => {
+      setIsRebuildingProcessing(true);
+    });
+
+    socket.on(SocketEventName.FinishAddPage, async(data) => {
+      await retrieveIndicesStatus();
+      setIsRebuildingProcessing(false);
+      setIsRebuildingCompleted(true);
+    });
+
+    socket.on(SocketEventName.RebuildingFailed, (data) => {
+      toastError(new Error(data.error), 'Rebuilding Index has failed.');
+    });
+
+    return () => {
+      socket.off(SocketEventName.AddPageProgress);
+      socket.off(SocketEventName.FinishAddPage);
+      socket.off(SocketEventName.RebuildingFailed);
+    };
+  }, [socket]);
+
+
+  const reconnect = async() => {
+    setIsReconnectingProcessing(true);
+
+    try {
+      await apiv3Post('/search/connection');
+    }
+    catch (e) {
+      toastError(e);
+      return;
+    }
+
+    // reload
+    window.location.reload();
+  };
+
+  const normalizeIndices = async() => {
+
+    try {
+      await apiv3Put('/search/indices', { operation: 'normalize' });
+    }
+    catch (e) {
+      toastError(e);
+    }
+
+    await retrieveIndicesStatus();
+
+    toastSuccess('Normalizing has succeeded');
+  };
+
+  const rebuildIndices = async() => {
+    setIsRebuildingProcessing(true);
+
+    try {
+      await apiv3Put('/search/indices', { operation: 'rebuild' });
+      toastSuccess('Rebuilding is requested');
+    }
+    catch (e) {
+      toastError(e);
+    }
+
+    await retrieveIndicesStatus();
+  };
+
+  const isErrorOccuredOnSearchService = !isSearchServiceReachable;
+
+  const isReconnectBtnEnabled = !isReconnectingProcessing && (!isInitialized || !isConnected || isErrorOccuredOnSearchService);
+
+  return (
+    <>
+      <div className="row">
+        <div className="col-md-12">
+          <StatusTable
+            isInitialized={isInitialized}
+            isErrorOccuredOnSearchService={isErrorOccuredOnSearchService}
+            isConnected={isConnected}
+            isConfigured={isConfigured}
+            isNormalized={isNormalized}
+            indicesData={indicesData}
+            aliasesData={aliasesData}
+          />
+        </div>
+      </div>
+
+      <hr />
+
+      {/* Controls */}
+      <div className="row">
+        <label className="col-md-3 col-form-label text-left text-md-right">{ t('full_text_search_management.reconnect') }</label>
+        <div className="col-md-6">
+          <ReconnectControls
+            isEnabled={isReconnectBtnEnabled}
+            isProcessing={isReconnectingProcessing}
+            onReconnectingRequested={reconnect}
+          />
+        </div>
+      </div>
+
+      <hr />
+
+      <div className="row">
+        <label className="col-md-3 col-form-label text-left text-md-right">{ t('full_text_search_management.normalize') }</label>
+        <div className="col-md-6">
+          <NormalizeIndicesControls
+            isRebuildingProcessing={isRebuildingProcessing}
+            isNormalized={isNormalized}
+            onNormalizingRequested={normalizeIndices}
+          />
+        </div>
+      </div>
+
+      <hr />
+
+      <div className="row">
+        <label className="col-md-3 col-form-label text-left text-md-right">{ t('full_text_search_management.rebuild') }</label>
+        <div className="col-md-6">
+          <RebuildIndexControls
+            isRebuildingProcessing={isRebuildingProcessing}
+            isRebuildingCompleted={isRebuildingCompleted}
+            isNormalized={isNormalized}
+            onRebuildingRequested={rebuildIndices}
+          />
+        </div>
+      </div>
+
+    </>
+  );
+
+};
+
+
+ElasticsearchManagement.propTypes = {
+
+};
+
+export default ElasticsearchManagement;

+ 22 - 26
packages/app/src/components/Admin/ElasticsearchManagement/RebuildIndexControls.jsx

@@ -1,11 +1,10 @@
 import React from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
-import AdminSocketIoContainer from '~/client/services/AdminSocketIoContainer';
+import { useAdminSocket } from '~/stores/socket-io';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
 import LabeledProgressBar from '../Common/LabeledProgressBar';
 
 class RebuildIndexControls extends React.Component {
@@ -25,24 +24,25 @@ class RebuildIndexControls extends React.Component {
   }
 
   initWebSockets() {
-    const socket = this.props.adminSocketIoContainer.getSocket();
-
-    socket.on('addPageProgress', (data) => {
-      this.setState({
-        total: data.totalCount,
-        current: data.count,
-        skip: data.skipped,
+    const { socket } = this.props;
+
+    if (socket != null) {
+      socket.on('addPageProgress', (data) => {
+        this.setState({
+          total: data.totalCount,
+          current: data.count,
+          skip: data.skipped,
+        });
       });
-    });
 
-    socket.on('finishAddPage', (data) => {
-      this.setState({
-        total: data.totalCount,
-        current: data.count,
-        skip: data.skipped,
+      socket.on('finishAddPage', (data) => {
+        this.setState({
+          total: data.totalCount,
+          current: data.count,
+          skip: data.skipped,
+        });
       });
-    });
-
+    }
   }
 
   renderProgressBar() {
@@ -109,24 +109,20 @@ class RebuildIndexControls extends React.Component {
 
 const RebuildIndexControlsFC = (props) => {
   const { t } = useTranslation();
-  return <RebuildIndexControls t={t} {...props} />;
+  const { data: socket } = useAdminSocket();
+  return <RebuildIndexControls t={t} socket={socket} {...props} />;
 };
 
 
-/**
- * Wrapper component for using unstated
- */
-const RebuildIndexControlsWrapper = withUnstatedContainers(RebuildIndexControlsFC, [AdminSocketIoContainer]);
-
 RebuildIndexControls.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  adminSocketIoContainer: PropTypes.instanceOf(AdminSocketIoContainer).isRequired,
 
   isRebuildingProcessing: PropTypes.bool.isRequired,
   isRebuildingCompleted: PropTypes.bool.isRequired,
 
   isNormalized: PropTypes.bool,
   onRebuildingRequested: PropTypes.func.isRequired,
+  socket: PropTypes.object,
 };
 
-export default RebuildIndexControlsWrapper;
+export default RebuildIndexControlsFC;

+ 4 - 2
packages/app/src/components/Admin/ExportArchiveData/SelectCollectionsModal.jsx

@@ -13,13 +13,15 @@ import { apiPost } from '~/client/util/apiv1-client';
 
 
 const GROUPS_PAGE = [
-  'pages', 'revisions', 'tags', 'pagetagrelations',
+  'pages', 'revisions', 'tags', 'pagetagrelations', 'pageredirects', 'comments', 'sharelinks',
 ];
 const GROUPS_USER = [
   'users', 'externalaccounts', 'usergroups', 'usergrouprelations',
+  'useruisettings', 'editorsettings', 'bookmarks', 'subscriptions',
+  'inappnotificationsettings',
 ];
 const GROUPS_CONFIG = [
-  'configs', 'updateposts', 'globalnotificationsettings',
+  'configs', 'updateposts', 'globalnotificationsettings', 'slackappintegrations',
 ];
 const ALL_GROUPED_COLLECTIONS = GROUPS_PAGE.concat(GROUPS_USER).concat(GROUPS_CONFIG);
 

+ 1 - 1
packages/app/src/components/Admin/ExportArchiveDataPage.jsx

@@ -15,7 +15,7 @@ import SelectCollectionsModal from './ExportArchiveData/SelectCollectionsModal';
 
 
 const IGNORED_COLLECTION_NAMES = [
-  'sessions',
+  'sessions', 'rlflx', 'activities',
 ];
 
 class ExportArchiveDataPage extends React.Component {

+ 23 - 1
packages/app/src/components/Admin/ImportData/ImportDataPageContents.jsx

@@ -1,14 +1,19 @@
-import React from 'react';
+import React, { useEffect } from 'react';
 
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 
 import AdminImportContainer from '~/client/services/AdminImportContainer';
+import { toastError } from '~/client/util/apiNotification';
+import { toArrayIfNot } from '~/utils/array-utils';
+import loggerFactory from '~/utils/logger';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
 import GrowiArchiveSection from './GrowiArchiveSection';
 
+const logger = loggerFactory('growi:importer');
+
 class ImportDataPageContents extends React.Component {
 
   render() {
@@ -242,6 +247,23 @@ ImportDataPageContents.propTypes = {
 const ImportDataPageContentsWrapperFc = (props) => {
   const { t } = useTranslation();
 
+  const { adminImportContainer } = props;
+
+  useEffect(() => {
+    const fetchImportSettingsData = async() => {
+      await adminImportContainer.retrieveImportSettingsData();
+    };
+
+    try {
+      fetchImportSettingsData();
+    }
+    catch (err) {
+      const errs = toArrayIfNot(err);
+      toastError(errs);
+      logger.error(errs);
+    }
+  }, [adminImportContainer]);
+
   return <ImportDataPageContents t={t} {...props} />;
 };
 

+ 0 - 52
packages/app/src/components/Admin/ImportDataPage.jsx

@@ -1,52 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import loggerFactory from '~/utils/logger';
-
-import { withUnstatedContainers } from '../UnstatedUtils';
-import { toArrayIfNot } from '~/utils/array-utils';
-import { withLoadingSppiner } from '../SuspenseUtils';
-
-import AdminImportContainer from '~/client/services/AdminImportContainer';
-import { toastError } from '~/client/util/apiNotification';
-
-import ImportDataPageContents from './ImportData/ImportDataPageContents';
-
-const logger = loggerFactory('growi:importer');
-
-let retrieveErrors = null;
-function ImportDataPage(props) {
-  const { adminImportContainer } = props;
-
-  if (adminImportContainer.state.esaTeamName === adminImportContainer.dummyEsaTeamName) {
-    throw (async() => {
-      try {
-        await adminImportContainer.retrieveImportSettingsData();
-      }
-      catch (err) {
-        const errs = toArrayIfNot(err);
-        toastError(errs);
-        logger.error(errs);
-        retrieveErrors = errs;
-        adminImportContainer.setState({ esaTeamName: adminImportContainer.dummyEsaTeamNameForError });
-      }
-    })();
-  }
-
-  if (adminImportContainer.state.esaTeamName === adminImportContainer.dummyEsaTeamNameForError) {
-    throw new Error(`${retrieveErrors.length} errors occured`);
-  }
-
-  return <ImportDataPageContents />;
-}
-
-ImportDataPage.propTypes = {
-  adminImportContainer: PropTypes.instanceOf(AdminImportContainer).isRequired,
-};
-
-
-/**
- * Wrapper component for using unstated
- */
-const ImportDataPageWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(ImportDataPage), [AdminImportContainer]);
-
-export default ImportDataPageWithUnstatedContainer;

+ 25 - 28
packages/app/src/components/Admin/LegacySlackIntegration/LegacySlackIntegration.jsx

@@ -1,43 +1,40 @@
-import React, { useMemo, useState } from 'react';
-import PropTypes from 'prop-types';
+import React, { useEffect, useMemo, useState } from 'react';
+
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
+import AdminSlackIntegrationLegacyContainer from '~/client/services/AdminSlackIntegrationLegacyContainer';
+import { toastError } from '~/client/util/apiNotification';
+import { toArrayIfNot } from '~/utils/array-utils';
 import loggerFactory from '~/utils/logger';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
-import { toastError } from '~/client/util/apiNotification';
-import { toArrayIfNot } from '~/utils/array-utils';
-import { withLoadingSppiner } from '../../SuspenseUtils';
 
-import AdminSlackIntegrationLegacyContainer from '~/client/services/AdminSlackIntegrationLegacyContainer';
 
 import SlackConfiguration from './SlackConfiguration';
 
 const logger = loggerFactory('growi:NotificationSetting');
 
-let retrieveErrors = null;
-function LegacySlackIntegration(props) {
+const LegacySlackIntegration = (props) => {
   const { t } = useTranslation();
   const { adminSlackIntegrationLegacyContainer } = props;
 
-  if (adminSlackIntegrationLegacyContainer.state.webhookUrl === adminSlackIntegrationLegacyContainer.dummyWebhookUrl) {
-    throw (async() => {
-      try {
-        await adminSlackIntegrationLegacyContainer.retrieveData();
-      }
-      catch (err) {
-        const errs = toArrayIfNot(err);
-        toastError(errs);
-        logger.error(errs);
-        retrieveErrors = errs;
-        adminSlackIntegrationLegacyContainer.setState({ webhookUrl: adminSlackIntegrationLegacyContainer.dummyWebhookUrlForError });
-      }
-    })();
-  }
-
-  if (adminSlackIntegrationLegacyContainer.state.webhookUrl === adminSlackIntegrationLegacyContainer.dummyWebhookUrlForError) {
-    throw new Error(`${retrieveErrors.length} errors occured`);
-  }
+
+  useEffect(() => {
+    const fetchLegacySlackIntegrationData = async() => {
+      await adminSlackIntegrationLegacyContainer.retrieveData();
+    };
+
+    try {
+      fetchLegacySlackIntegrationData();
+    }
+    catch (err) {
+      const errs = toArrayIfNot(err);
+      toastError(errs);
+      logger.error(errs);
+    }
+  }, [adminSlackIntegrationLegacyContainer]);
+
 
   const isDisabled = adminSlackIntegrationLegacyContainer.state.isSlackbotConfigured;
 
@@ -60,9 +57,9 @@ function LegacySlackIntegration(props) {
       <SlackConfiguration />
     </div>
   );
-}
+};
 
-const LegacySlackIntegrationWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(LegacySlackIntegration), [AdminSlackIntegrationLegacyContainer]);
+const LegacySlackIntegrationWithUnstatedContainer = withUnstatedContainers(LegacySlackIntegration, [AdminSlackIntegrationLegacyContainer]);
 
 LegacySlackIntegration.propTypes = {
   adminSlackIntegrationLegacyContainer: PropTypes.instanceOf(AdminSlackIntegrationLegacyContainer).isRequired,

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

@@ -1,7 +1,7 @@
 import React from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 import AdminSlackIntegrationLegacyContainer from '~/client/services/AdminSlackIntegrationLegacyContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';

+ 0 - 48
packages/app/src/components/Admin/MarkdownSetting/MarkDownSetting.jsx

@@ -1,48 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import loggerFactory from '~/utils/logger';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-import { toastError } from '~/client/util/apiNotification';
-import { toArrayIfNot } from '~/utils/array-utils';
-import { withLoadingSppiner } from '../../SuspenseUtils';
-
-import MarkDownSettingContents from './MarkDownSettingContents';
-import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
-
-const logger = loggerFactory('growi:MarkDown');
-
-let retrieveErrors = null;
-function MarkdownSetting(props) {
-  const { adminMarkDownContainer } = props;
-
-  if (adminMarkDownContainer.state.isEnabledLinebreaks === adminMarkDownContainer.dummyIsEnabledLinebreaks) {
-    throw (async() => {
-      try {
-        await adminMarkDownContainer.retrieveMarkdownData();
-      }
-      catch (err) {
-        const errs = toArrayIfNot(err);
-        toastError(errs);
-        logger.error(errs);
-        retrieveErrors = errs;
-        adminMarkDownContainer.setState({ isEnabledLinebreaks: adminMarkDownContainer.dummyIsEnabledLinebreaksForError });
-      }
-    })();
-  }
-
-  if (adminMarkDownContainer.state.isEnabledLinebreaks === adminMarkDownContainer.dummyIsEnabledLinebreaksForError) {
-    throw new Error(`${retrieveErrors.length} errors occured`);
-  }
-
-  return <MarkDownSettingContents />;
-}
-
-const MarkdownSettingWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(MarkdownSetting), [AdminMarkDownContainer]);
-
-MarkdownSetting.propTypes = {
-  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
-};
-
-export default MarkdownSettingWithUnstatedContainer;

+ 36 - 3
packages/app/src/components/Admin/MarkdownSetting/MarkDownSettingContents.tsx

@@ -1,15 +1,44 @@
-import React from 'react';
+import React, { useEffect } from 'react';
 
 import { useTranslation } from 'next-i18next';
 import { Card, CardBody } from 'reactstrap';
 
+import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
+import { toastError } from '~/client/util/apiNotification';
+import { toArrayIfNot } from '~/utils/array-utils';
+import loggerFactory from '~/utils/logger';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
+
 import IndentForm from './IndentForm';
 import LineBreakForm from './LineBreakForm';
 import PresentationForm from './PresentationForm';
 import XssForm from './XssForm';
 
-const MarkDownSettingContents = React.memo((): JSX.Element => {
+const logger = loggerFactory('growi:MarkDown');
+
+type Props ={
+  adminMarkDownContainer: AdminMarkDownContainer
+}
+
+const MarkDownSettingContents = React.memo((props: Props): JSX.Element => {
   const { t } = useTranslation();
+  const { adminMarkDownContainer } = props;
+
+  useEffect(() => {
+    const fetchMarkdownData = async() => {
+      await adminMarkDownContainer.retrieveMarkdownData();
+    };
+
+    try {
+      fetchMarkdownData();
+    }
+    catch (err) {
+      const errs = toArrayIfNot(err);
+      toastError(errs);
+      logger.error(errs);
+    }
+  }, [adminMarkDownContainer]);
 
   return (
     <div data-testid="admin-markdown">
@@ -45,4 +74,8 @@ const MarkDownSettingContents = React.memo((): JSX.Element => {
 });
 MarkDownSettingContents.displayName = 'MarkDownSettingContents';
 
-export default MarkDownSettingContents;
+
+const MarkdownSettingWithUnstatedContainer = withUnstatedContainers(MarkDownSettingContents, [AdminMarkDownContainer]);
+
+
+export default MarkdownSettingWithUnstatedContainer;

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

@@ -1,7 +1,7 @@
 import React from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 import urljoin from 'url-join';
 
 import AppContainer from '~/client/services/AppContainer';
@@ -172,7 +172,7 @@ class ManageGlobalNotification extends React.Component {
                 <>
                   <div className="input-group notify-to-option" id="mail-input">
                     <div className="input-group-prepend">
-                      <span className="input-group-text" id="mail-addon"><i className="ti-email" /></span>
+                      <span className="input-group-text" id="mail-addon"><i className="ti ti-email" /></span>
                     </div>
                     <input
                       className="form-control"

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

@@ -1,13 +1,13 @@
 /* eslint-disable react/no-danger */
 import React from 'react';
+
 import PropTypes from 'prop-types';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
+import AdminBasicSecurityContainer from '~/client/services/AdminBasicSecurityContainer';
 import { toastError } from '~/client/util/apiNotification';
 import { toArrayIfNot } from '~/utils/array-utils';
-import { withLoadingSppiner } from '../../SuspenseUtils';
 
-import AdminBasicSecurityContainer from '~/client/services/AdminBasicSecurityContainer';
+import { withUnstatedContainers } from '../../UnstatedUtils';
 
 import BasicSecurityManagementContents from './BasicSecuritySettingContents';
 
@@ -44,7 +44,7 @@ BasicSecurityManagement.propTypes = {
   adminBasicSecurityContainer: PropTypes.instanceOf(AdminBasicSecurityContainer).isRequired,
 };
 
-const BasicSecurityManagementWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(BasicSecurityManagement), [
+const BasicSecurityManagementWithUnstatedContainer = withUnstatedContainers(BasicSecurityManagement, [
   AdminBasicSecurityContainer,
 ]);
 

+ 5 - 4
packages/app/src/components/Admin/Security/GitHubSecuritySetting.jsx

@@ -1,13 +1,14 @@
 /* eslint-disable react/no-danger */
 import React from 'react';
+
 import PropTypes from 'prop-types';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
+import AdminGitHubSecurityContainer from '~/client/services/AdminGitHubSecurityContainer';
 import { toastError } from '~/client/util/apiNotification';
 import { toArrayIfNot } from '~/utils/array-utils';
-import { withLoadingSppiner } from '../../SuspenseUtils';
 
-import AdminGitHubSecurityContainer from '~/client/services/AdminGitHubSecurityContainer';
+import { withUnstatedContainers } from '../../UnstatedUtils';
+
 
 import GitHubSecuritySettingContents from './GitHubSecuritySettingContents';
 
@@ -40,7 +41,7 @@ GitHubSecurityManagement.propTypes = {
   adminGitHubSecurityContainer: PropTypes.instanceOf(AdminGitHubSecurityContainer).isRequired,
 };
 
-const GitHubSecurityManagementWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(GitHubSecurityManagement), [
+const GitHubSecurityManagementWithUnstatedContainer = withUnstatedContainers(GitHubSecurityManagement, [
   AdminGitHubSecurityContainer,
 ]);
 

+ 5 - 4
packages/app/src/components/Admin/Security/GoogleSecuritySetting.jsx

@@ -1,13 +1,14 @@
 /* eslint-disable react/no-danger */
 import React from 'react';
+
 import PropTypes from 'prop-types';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
+import AdminGoogleSecurityContainer from '~/client/services/AdminGoogleSecurityContainer';
 import { toastError } from '~/client/util/apiNotification';
 import { toArrayIfNot } from '~/utils/array-utils';
-import { withLoadingSppiner } from '../../SuspenseUtils';
 
-import AdminGoogleSecurityContainer from '~/client/services/AdminGoogleSecurityContainer';
+import { withUnstatedContainers } from '../../UnstatedUtils';
+
 import GoogleSecurityManagementContents from './GoogleSecuritySettingContents';
 
 let retrieveErrors = null;
@@ -39,7 +40,7 @@ GoogleSecurityManagement.propTypes = {
   adminGoogleSecurityContainer: PropTypes.instanceOf(AdminGoogleSecurityContainer).isRequired,
 };
 
-const GoogleSecurityManagementWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(GoogleSecurityManagement), [
+const GoogleSecurityManagementWithUnstatedContainer = withUnstatedContainers(GoogleSecurityManagement, [
   AdminGoogleSecurityContainer,
 ]);
 

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

@@ -1,12 +1,12 @@
 import React from 'react';
+
 import PropTypes from 'prop-types';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
+import AdminLdapSecurityContainer from '~/client/services/AdminLdapSecurityContainer';
 import { toastError } from '~/client/util/apiNotification';
 import { toArrayIfNot } from '~/utils/array-utils';
-import { withLoadingSppiner } from '../../SuspenseUtils';
 
-import AdminLdapSecurityContainer from '~/client/services/AdminLdapSecurityContainer';
+import { withUnstatedContainers } from '../../UnstatedUtils';
 
 import LdapSecuritySettingContents from './LdapSecuritySettingContents';
 
@@ -38,7 +38,7 @@ LdapSecuritySetting.propTypes = {
   adminLdapSecurityContainer: PropTypes.instanceOf(AdminLdapSecurityContainer).isRequired,
 };
 
-const LdapSecuritySettingWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(LdapSecuritySetting), [
+const LdapSecuritySettingWithUnstatedContainer = withUnstatedContainers(LdapSecuritySetting, [
   AdminLdapSecurityContainer,
 ]);
 

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

@@ -1,13 +1,13 @@
 /* eslint-disable react/no-danger */
 import React from 'react';
+
 import PropTypes from 'prop-types';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
+import AdminLocalSecurityContainer from '~/client/services/AdminLocalSecurityContainer';
 import { toastError } from '~/client/util/apiNotification';
 import { toArrayIfNot } from '~/utils/array-utils';
-import { withLoadingSppiner } from '../../SuspenseUtils';
 
-import AdminLocalSecurityContainer from '~/client/services/AdminLocalSecurityContainer';
+import { withUnstatedContainers } from '../../UnstatedUtils';
 
 import LocalSecuritySettingContents from './LocalSecuritySettingContents';
 
@@ -39,7 +39,7 @@ LocalSecuritySetting.propTypes = {
   adminLocalSecurityContainer: PropTypes.instanceOf(AdminLocalSecurityContainer).isRequired,
 };
 
-const LocalSecuritySettingWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(LocalSecuritySetting), [
+const LocalSecuritySettingWithUnstatedContainer = withUnstatedContainers(LocalSecuritySetting, [
   AdminLocalSecurityContainer,
 ]);
 

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

@@ -1,13 +1,13 @@
 /* eslint-disable react/no-danger */
 import React from 'react';
+
 import PropTypes from 'prop-types';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
+import AdminOidcSecurityContainer from '~/client/services/AdminOidcSecurityContainer';
 import { toastError } from '~/client/util/apiNotification';
 import { toArrayIfNot } from '~/utils/array-utils';
-import { withLoadingSppiner } from '../../SuspenseUtils';
 
-import AdminOidcSecurityContainer from '~/client/services/AdminOidcSecurityContainer';
+import { withUnstatedContainers } from '../../UnstatedUtils';
 
 import OidcSecurityManagementContents from './OidcSecuritySettingContents';
 
@@ -39,7 +39,7 @@ OidcSecurityManagement.propTypes = {
   adminOidcSecurityContainer: PropTypes.instanceOf(AdminOidcSecurityContainer).isRequired,
 };
 
-const OidcSecurityManagementWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(OidcSecurityManagement), [
+const OidcSecurityManagementWithUnstatedContainer = withUnstatedContainers(OidcSecurityManagement, [
   AdminOidcSecurityContainer,
 ]);
 

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

@@ -1,13 +1,13 @@
 /* eslint-disable react/no-danger */
 import React from 'react';
-import PropTypes from 'prop-types';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
-import { toastError } from '~/client/util/apiNotification';
+import PropTypes from 'prop-types';
 
 import AdminSamlSecurityContainer from '~/client/services/AdminSamlSecurityContainer';
+import { toastError } from '~/client/util/apiNotification';
 import { toArrayIfNot } from '~/utils/array-utils';
-import { withLoadingSppiner } from '../../SuspenseUtils';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
 
 import SamlSecuritySettingContents from './SamlSecuritySettingContents';
 
@@ -39,7 +39,7 @@ SamlSecurityManagement.propTypes = {
   adminSamlSecurityContainer: PropTypes.instanceOf(AdminSamlSecurityContainer).isRequired,
 };
 
-const SamlSecurityManagementWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(SamlSecurityManagement), [
+const SamlSecurityManagementWithUnstatedContainer = withUnstatedContainers(SamlSecurityManagement, [
   AdminSamlSecurityContainer,
 ]);
 

+ 5 - 4
packages/app/src/components/Admin/Security/SecurityManagement.jsx

@@ -1,12 +1,13 @@
 import React from 'react';
+
 import PropTypes from 'prop-types';
+
+import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import { toastError } from '~/client/util/apiNotification';
+import { toArrayIfNot } from '~/utils/array-utils';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
-import { toArrayIfNot } from '~/utils/array-utils';
-import { withLoadingSppiner } from '../../SuspenseUtils';
 
-import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import SecurityManagementContents from './SecurityManagementContents';
 
 let retrieveErrors = null;
@@ -40,6 +41,6 @@ SecurityManagement.propTypes = {
   adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
 };
 
-const SecurityManagementWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(SecurityManagement), [AdminGeneralSecurityContainer]);
+const SecurityManagementWithUnstatedContainer = withUnstatedContainers(SecurityManagement, [AdminGeneralSecurityContainer]);
 
 export default SecurityManagementWithUnstatedContainer;

+ 5 - 4
packages/app/src/components/Admin/Security/TwitterSecuritySetting.jsx

@@ -1,13 +1,14 @@
 /* eslint-disable react/no-danger */
 import React from 'react';
+
 import PropTypes from 'prop-types';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
+import AdminTwitterSecurityContainer from '~/client/services/AdminTwitterSecurityContainer';
 import { toastError } from '~/client/util/apiNotification';
 import { toArrayIfNot } from '~/utils/array-utils';
-import { withLoadingSppiner } from '../../SuspenseUtils';
 
-import AdminTwitterSecurityContainer from '~/client/services/AdminTwitterSecurityContainer';
+import { withUnstatedContainers } from '../../UnstatedUtils';
+
 
 import TwitterSecuritySettingContents from './TwitterSecuritySettingContents';
 
@@ -41,7 +42,7 @@ TwitterSecurityManagement.propTypes = {
   adminTwitterSecurityContainer: PropTypes.instanceOf(AdminTwitterSecurityContainer).isRequired,
 };
 
-const TwitterSecurityManagementWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(TwitterSecurityManagement), [
+const TwitterSecurityManagementWithUnstatedContainer = withUnstatedContainers(TwitterSecurityManagement, [
   AdminTwitterSecurityContainer,
 ]);
 

+ 1 - 1
packages/app/src/components/Admin/SlackIntegration/BotTypeCard.jsx

@@ -31,7 +31,7 @@ const botDetails = {
 };
 
 const BotTypeCard = (props) => {
-  const { t } = useTranslation('admin');
+  const { t } = useTranslation();
 
   const isBotTypeOfficial = props.botType === SlackbotType.OFFICIAL;
 

+ 2 - 1
packages/app/src/components/Admin/SlackIntegration/Bridge.jsx

@@ -1,4 +1,5 @@
 import React from 'react';
+
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import { UncontrolledTooltip } from 'reactstrap';
@@ -20,7 +21,7 @@ const BridgeCore = (props) => {
   return (
     <>
       <div id="grw-bridge-container" className={`grw-bridge-container ${withProxy ? 'with-proxy' : ''}`}>
-        <p className="label">
+        <p className={`${withProxy ? 'mt-0' : 'mt-2'}`}>
           <i className={iconClass} />
           <small
             className="ml-2 d-none d-lg-inline"

+ 3 - 2
packages/app/src/components/Admin/SlackIntegration/ConfirmBotChangeModal.jsx

@@ -1,12 +1,13 @@
 import React from 'react';
-import PropTypes from 'prop-types';
+
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 
 const ConfirmBotChangeModal = (props) => {
-  const { t } = useTranslation('admin');
+  const { t } = useTranslation();
 
   const handleCancelButton = () => {
     if (props.onCancelClick != null) {

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

@@ -1,14 +1,13 @@
 import React, { useState, useEffect, useCallback } from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Delete, apiv3Put } from '~/client/util/apiv3-client';
+import { useAppTitle } from '~/stores/context';
 import loggerFactory from '~/utils/logger';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
 
 import CustomBotWithProxyConnectionStatus from './CustomBotWithProxyConnectionStatus';
 import DeleteSlackBotSettingsModal from './DeleteSlackBotSettingsModal';
@@ -19,7 +18,7 @@ const logger = loggerFactory('growi:cli:SlackIntegration:CustomBotWithProxySetti
 
 const CustomBotWithProxySettings = (props) => {
   const {
-    appContainer, slackAppIntegrations, proxyServerUri,
+    slackAppIntegrations, proxyServerUri,
     onClickAddSlackWorkspaceBtn, onPrimaryUpdated,
     connectionStatuses, onUpdateTokens, onSubmitForm,
   } = props;
@@ -27,6 +26,7 @@ const CustomBotWithProxySettings = (props) => {
   const [integrationIdToDelete, setIntegrationIdToDelete] = useState(null);
   const [siteName, setSiteName] = useState('');
   const { t } = useTranslation();
+  const { data: appTitle } = useAppTitle();
 
   // componentDidUpdate
   useEffect(() => {
@@ -86,9 +86,8 @@ const CustomBotWithProxySettings = (props) => {
   };
 
   useEffect(() => {
-    const siteName = appContainer.config.crowi.title;
-    setSiteName(siteName);
-  }, [appContainer]);
+    setSiteName(appTitle);
+  }, [appTitle]);
 
   return (
     <>
@@ -183,14 +182,11 @@ const CustomBotWithProxySettings = (props) => {
   );
 };
 
-const CustomBotWithProxySettingsWrapper = withUnstatedContainers(CustomBotWithProxySettings, [AppContainer]);
-
 CustomBotWithProxySettings.defaultProps = {
   slackAppIntegrations: [],
 };
 
 CustomBotWithProxySettings.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   slackAppIntegrations: PropTypes.array,
   proxyServerUri: PropTypes.string,
   onClickAddSlackWorkspaceBtn: PropTypes.func,
@@ -201,4 +197,4 @@ CustomBotWithProxySettings.propTypes = {
   onUpdateTokens: PropTypes.func,
 };
 
-export default CustomBotWithProxySettingsWrapper;
+export default CustomBotWithProxySettings;

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

@@ -1,9 +1,8 @@
 import React, { useState, useEffect } from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Put } from '~/client/util/apiv3-client';
 
@@ -13,7 +12,7 @@ import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 const CustomBotWithoutProxySecretTokenSection = (props) => {
   const {
-    appContainer, slackSigningSecret, slackBotToken, slackSigningSecretEnv, slackBotTokenEnv, onUpdatedSecretToken,
+    slackSigningSecret, slackBotToken, slackSigningSecretEnv, slackBotTokenEnv, onUpdatedSecretToken,
   } = props;
   const { t } = useTranslation();
 
@@ -113,11 +112,7 @@ const CustomBotWithoutProxySecretTokenSection = (props) => {
   );
 };
 
-const CustomBotWithoutProxySecretTokenSectionWrapper = withUnstatedContainers(CustomBotWithoutProxySecretTokenSection, [AppContainer]);
-
 CustomBotWithoutProxySecretTokenSection.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-
   onUpdatedSecretToken: PropTypes.func,
   slackSigningSecret: PropTypes.string,
   slackSigningSecretEnv: PropTypes.string,
@@ -125,4 +120,4 @@ CustomBotWithoutProxySecretTokenSection.propTypes = {
   slackBotTokenEnv: PropTypes.string,
 };
 
-export default CustomBotWithoutProxySecretTokenSectionWrapper;
+export default CustomBotWithoutProxySecretTokenSection;

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

@@ -1,20 +1,24 @@
 import React, { useState, useEffect } from 'react';
+
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
-import AppContainer from '~/client/services/AppContainer';
+
+import { useAppTitle } from '~/stores/context';
+
 import { withUnstatedContainers } from '../../UnstatedUtils';
-import CustomBotWithoutProxySettingsAccordion, { botInstallationStep } from './CustomBotWithoutProxySettingsAccordion';
+
 import CustomBotWithoutProxyConnectionStatus from './CustomBotWithoutProxyConnectionStatus';
+import CustomBotWithoutProxySettingsAccordion, { botInstallationStep } from './CustomBotWithoutProxySettingsAccordion';
 
 const CustomBotWithoutProxySettings = (props) => {
-  const { appContainer, connectionStatuses } = props;
+  const { connectionStatuses } = props;
   const { t } = useTranslation();
+  const { data: appTitle } = useAppTitle();
   const [siteName, setSiteName] = useState('');
 
   useEffect(() => {
-    const siteName = appContainer.config.crowi.title;
-    setSiteName(siteName);
-  }, [appContainer]);
+    setSiteName(appTitle);
+  }, [appTitle]);
 
   const workspaceName = connectionStatuses[props.slackBotToken]?.workspaceName;
 
@@ -58,10 +62,8 @@ const CustomBotWithoutProxySettings = (props) => {
   );
 };
 
-const CustomBotWithoutProxySettingsWrapper = withUnstatedContainers(CustomBotWithoutProxySettings, [AppContainer]);
 
 CustomBotWithoutProxySettings.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 
   slackSigningSecret: PropTypes.string,
   slackSigningSecretEnv: PropTypes.string,
@@ -75,4 +77,4 @@ CustomBotWithoutProxySettings.propTypes = {
   eventActionsPermission: PropTypes.object,
 };
 
-export default CustomBotWithoutProxySettingsWrapper;
+export default CustomBotWithoutProxySettings;

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

@@ -1,12 +1,10 @@
 import React, { useState } from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
-import AppContainer from '~/client/services/AppContainer';
 import { apiv3Post } from '~/client/util/apiv3-client';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
 import Accordion from '../Common/Accordion';
 
 import CustomBotWithoutProxySecretTokenSection from './CustomBotWithoutProxySecretTokenSection';
@@ -25,7 +23,7 @@ export const botInstallationStep = {
 
 const CustomBotWithoutProxySettingsAccordion = (props) => {
   const {
-    appContainer, activeStep, onTestConnectionInvoked,
+    activeStep, onTestConnectionInvoked,
     slackSigningSecret, slackBotToken, slackSigningSecretEnv, slackBotTokenEnv, commandPermission, eventActionsPermission,
   } = props;
   const successMessage = 'Successfully sent to Slack workspace.';
@@ -190,12 +188,8 @@ const CustomBotWithoutProxySettingsAccordion = (props) => {
 };
 
 
-const CustomBotWithoutProxySettingsAccordionWrapper = withUnstatedContainers(CustomBotWithoutProxySettingsAccordion, [AppContainer]);
-
-
 CustomBotWithoutProxySettingsAccordion.propTypes = {
   activeStep: PropTypes.oneOf(Object.values(botInstallationStep)).isRequired,
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 
   onUpdatedSecretToken: PropTypes.func,
   onTestConnectionInvoked: PropTypes.func,
@@ -208,4 +202,4 @@ CustomBotWithoutProxySettingsAccordion.propTypes = {
   eventActionsPermission: PropTypes.object,
 };
 
-export default CustomBotWithoutProxySettingsAccordionWrapper;
+export default CustomBotWithoutProxySettingsAccordion;

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

@@ -1,17 +1,15 @@
 import React, { useState, useEffect, useCallback } from 'react';
 
 import { SlackbotType } from '@growi/slack';
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Delete, apiv3Put } from '~/client/util/apiv3-client';
+import { useAppTitle } from '~/stores/context';
 import loggerFactory from '~/utils/logger';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
-
 
 import CustomBotWithProxyConnectionStatus from './CustomBotWithProxyConnectionStatus';
 import DeleteSlackBotSettingsModal from './DeleteSlackBotSettingsModal';
@@ -22,13 +20,14 @@ const logger = loggerFactory('growi:cli:SlackIntegration:OfficialBotSettings');
 
 const OfficialBotSettings = (props) => {
   const {
-    appContainer, slackAppIntegrations,
+    slackAppIntegrations,
     onClickAddSlackWorkspaceBtn, onPrimaryUpdated,
     connectionStatuses, onUpdateTokens, onSubmitForm,
   } = props;
   const [siteName, setSiteName] = useState('');
   const [integrationIdToDelete, setIntegrationIdToDelete] = useState(null);
   const { t } = useTranslation();
+  const { data: appTitle } = useAppTitle();
 
   const addSlackAppIntegrationHandler = async() => {
     if (onClickAddSlackWorkspaceBtn != null) {
@@ -69,10 +68,10 @@ const OfficialBotSettings = (props) => {
     }
   };
 
+
   useEffect(() => {
-    const siteName = appContainer.config.crowi.title;
-    setSiteName(siteName);
-  }, [appContainer]);
+    setSiteName(appTitle);
+  }, [appTitle]);
 
   return (
     <>
@@ -151,14 +150,12 @@ const OfficialBotSettings = (props) => {
   );
 };
 
-const OfficialBotSettingsWrapper = withUnstatedContainers(OfficialBotSettings, [AppContainer]);
 
 OfficialBotSettings.defaultProps = {
   slackAppIntegrations: [],
 };
 
 OfficialBotSettings.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 
   slackAppIntegrations: PropTypes.array,
   onClickAddSlackWorkspaceBtn: PropTypes.func,
@@ -169,4 +166,4 @@ OfficialBotSettings.propTypes = {
   onSubmitForm: PropTypes.func,
 };
 
-export default OfficialBotSettingsWrapper;
+export default OfficialBotSettings;

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

@@ -1,18 +1,14 @@
 import React, { useState, useEffect, useCallback } from 'react';
 
 import { SlackbotType } from '@growi/slack';
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
 
 
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import {
   apiv3Delete, apiv3Get, apiv3Post, apiv3Put,
 } from '~/client/util/apiv3-client';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
-
 import BotTypeCard from './BotTypeCard';
 import ConfirmBotChangeModal from './ConfirmBotChangeModal';
 import CustomBotWithProxySettings from './CustomBotWithProxySettings';
@@ -23,9 +19,8 @@ import OfficialBotSettings from './OfficialBotSettings';
 
 const botTypes = Object.values(SlackbotType);
 
-const SlackIntegration = (props) => {
+const SlackIntegration = () => {
 
-  const { appContainer } = props;
   const { t } = useTranslation();
   const [currentBotType, setCurrentBotType] = useState(null);
   const [selectedBotType, setSelectedBotType] = useState(null);
@@ -256,10 +251,4 @@ const SlackIntegration = (props) => {
   );
 };
 
-const SlackIntegrationWrapper = withUnstatedContainers(SlackIntegration, [AppContainer]);
-
-SlackIntegration.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-};
-
-export default SlackIntegrationWrapper;
+export default SlackIntegration;

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

@@ -2,14 +2,14 @@
 import React, { useState, useCallback } from 'react';
 
 import { SlackbotType } from '@growi/slack';
+import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
-import { useTranslation } from 'next-i18next';
 import { Tooltip } from 'reactstrap';
 
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Put, apiv3Post } from '~/client/util/apiv3-client';
+import { useSiteUrl } from '~/stores/context';
 import loggerFactory from '~/utils/logger';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -145,7 +145,7 @@ const CustomCopyToClipBoard = (props) => {
 
 const GeneratingTokensAndRegisteringProxyServiceProcess = withUnstatedContainers((props) => {
   const { t } = useTranslation();
-  const { appContainer, slackAppIntegrationId } = props;
+  const { slackAppIntegrationId } = props;
 
   const regenerateTokensHandler = async() => {
     try {
@@ -231,7 +231,7 @@ const GeneratingTokensAndRegisteringProxyServiceProcess = withUnstatedContainers
     </div>
 
   );
-}, [AppContainer]);
+}, []);
 
 const TestProcess = ({
   slackAppIntegrationId, onSubmitForm, onSubmitFormFailed, isLatestConnectionSuccess,
@@ -313,6 +313,7 @@ const TestProcess = ({
 
 const WithProxyAccordions = (props) => {
   const { t } = useTranslation();
+  const { data: siteUrl } = useSiteUrl();
   const [isLatestConnectionSuccess, setIsLatestConnectionSuccess] = useState(false);
 
   const submitForm = () => {
@@ -334,7 +335,7 @@ const WithProxyAccordions = (props) => {
     '②': {
       title: 'register_for_growi_official_bot_proxy_service',
       content: <GeneratingTokensAndRegisteringProxyServiceProcess
-        growiUrl={props.appContainer.config.crowi.url}
+        growiUrl={siteUrl}
         slackAppIntegrationId={props.slackAppIntegrationId}
         tokenPtoG={props.tokenPtoG}
         tokenGtoP={props.tokenGtoP}
@@ -373,7 +374,7 @@ const WithProxyAccordions = (props) => {
     '③': {
       title: 'register_for_growi_custom_bot_proxy',
       content: <GeneratingTokensAndRegisteringProxyServiceProcess
-        growiUrl={props.appContainer.config.crowi.url}
+        growiUrl={siteUrl}
         slackAppIntegrationId={props.slackAppIntegrationId}
         tokenPtoG={props.tokenPtoG}
         tokenGtoP={props.tokenGtoP}
@@ -434,9 +435,7 @@ const WithProxyAccordions = (props) => {
 /**
  * Wrapper component for using unstated
  */
-const WithProxyAccordionsWrapper = withUnstatedContainers(WithProxyAccordions, [AppContainer]);
 WithProxyAccordions.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   botType: PropTypes.oneOf(Object.values(SlackbotType)).isRequired,
   slackAppIntegrationId: PropTypes.string.isRequired,
   tokenPtoG: PropTypes.string,
@@ -446,4 +445,4 @@ WithProxyAccordions.propTypes = {
   permissionsForSlackEventActions: PropTypes.object.isRequired,
 };
 
-export default WithProxyAccordionsWrapper;
+export default WithProxyAccordions;

+ 2 - 2
packages/app/src/components/Admin/UserGroup/UserGroupDeleteModal.tsx

@@ -135,7 +135,7 @@ const UserGroupDeleteModal: FC<Props> = (props: Props) => {
         onChange={handleActionChange}
       >
         <option value="" disabled>{t('admin:user_group_management.delete_modal.dropdown_desc')}</option>
-        {...options}
+        {options}
       </select>
     );
   }, [availableOptions, actionName, handleActionChange, t]);
@@ -164,7 +164,7 @@ const UserGroupDeleteModal: FC<Props> = (props: Props) => {
         onChange={handleGroupChange}
       >
         <option value="" disabled>{defaultOptionText}</option>
-        {...options}
+        {options}
       </select>
     );
   }, [deleteUserGroup, userGroups, t, actionName, transferToUserGroupId, handleGroupChange]);

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

@@ -2,8 +2,8 @@ import React from 'react';
 
 import { UserPicture } from '@growi/ui';
 import dateFnsFormat from 'date-fns/format';
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 import AdminUserGroupDetailContainer from '~/client/services/AdminUserGroupDetailContainer';
 import AppContainer from '~/client/services/AppContainer';
@@ -96,7 +96,7 @@ class UserGroupUserTable extends React.Component {
             <td></td>
             <td className="text-center">
               <button className="btn btn-outline-secondary" type="button" onClick={adminUserGroupDetailContainer.openUserGroupUserModal}>
-                <i className="ti-plus"></i>
+                <i className="ti ti-plus"></i>
               </button>
             </td>
             <td></td>

+ 0 - 49
packages/app/src/components/BasicLayout.tsx

@@ -1,49 +0,0 @@
-import React, { ReactNode } from 'react';
-
-import dynamic from 'next/dynamic';
-
-import { GrowiNavbar } from './Navbar/GrowiNavbar';
-import { RawLayout } from './RawLayout';
-import Sidebar from './Sidebar';
-
-
-type Props = {
-  title: string
-  className?: string,
-  children?: ReactNode
-}
-
-export const BasicLayout = ({ children, title, className }: Props): JSX.Element => {
-
-  // const HotkeysManager = dynamic(() => import('../client/js/components/Hotkeys/HotkeysManager'), { ssr: false });
-  // const PageCreateModal = dynamic(() => import('../client/js/components/PageCreateModal'), { ssr: false });
-  const GrowiNavbarBottom = dynamic(() => import('./Navbar/GrowiNavbarBottom').then(mod => mod.GrowiNavbarBottom), { ssr: false });
-  const ShortcutsModal = dynamic(() => import('./ShortcutsModal'), { ssr: false });
-  const SystemVersion = dynamic(() => import('./SystemVersion'), { ssr: false });
-
-  return (
-    <>
-      <RawLayout title={title} className={className}>
-        <GrowiNavbar />
-
-        <div className="page-wrapper d-flex d-print-block">
-          <div className="grw-sidebar-wrapper">
-            <Sidebar />
-          </div>
-
-          <div className="flex-fill mw-0">
-            {children}
-          </div>
-        </div>
-
-        <GrowiNavbarBottom />
-      </RawLayout>
-
-      {/* <PageCreateModal /> */}
-      {/* <HotkeysManager /> */}
-
-      <ShortcutsModal />
-      <SystemVersion />
-    </>
-  );
-};

+ 1 - 1
packages/app/src/components/BookmarkButtons.tsx

@@ -57,7 +57,7 @@ const BookmarkButtons: FC<Props> = (props: Props) => {
         type="button"
         id="bookmark-button"
         onClick={handleClick}
-        className={`shadow-none btn ${styles['btn-bookmark']} border-0
+        className={`shadow-none btn btn-bookmark ${styles['btn-bookmark']} border-0
           ${isBookmarked ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`}
       >
         <i className={`fa ${isBookmarked ? 'fa-bookmark' : 'fa-bookmark-o'}`}></i>

+ 1 - 1
packages/app/src/components/Common/Dropdown/PageItemControl.tsx

@@ -338,7 +338,7 @@ export const PageItemControlSubstance = (props: PageItemControlSubstanceProps):
 };
 
 
-type PageItemControlProps = CommonProps & {
+export type PageItemControlProps = CommonProps & {
   pageId?: string,
   children?: React.ReactNode,
   operationProcessData?: IPageOperationProcessData,

+ 127 - 0
packages/app/src/components/Common/ImageCropModal.tsx

@@ -0,0 +1,127 @@
+import React, {
+  FC, useCallback, useEffect, useState,
+} from 'react';
+
+import canvasToBlob from 'async-canvas-to-blob';
+import { useTranslation } from 'react-i18next';
+import ReactCrop from 'react-image-crop';
+import {
+  Modal,
+  ModalHeader,
+  ModalBody,
+  ModalFooter,
+} from 'reactstrap';
+
+import { toastError } from '~/client/util/apiNotification';
+import loggerFactory from '~/utils/logger';
+import 'react-image-crop/dist/ReactCrop.css';
+
+const logger = loggerFactory('growi:ImageCropModal');
+
+interface ICropOptions {
+  aspect: number
+  unit: string,
+  x: number
+  y: number
+  width: number,
+  height: number,
+}
+
+type CropOptions = ICropOptions | null
+
+type Props = {
+  isShow: boolean,
+  src: string | ArrayBuffer | null,
+  onModalClose: () => void,
+  onCropCompleted: (res: any) => void,
+  isCircular: boolean,
+}
+const ImageCropModal: FC<Props> = (props: Props) => {
+
+  const {
+    isShow, src, onModalClose, onCropCompleted, isCircular,
+  } = props;
+
+  const [imageRef, setImageRef] = useState<HTMLImageElement>();
+  const [cropOptions, setCropOtions] = useState<CropOptions>(null);
+  const { t } = useTranslation();
+  const reset = useCallback(() => {
+    if (imageRef) {
+      const size = Math.min(imageRef.width, imageRef.height);
+      setCropOtions({
+        aspect: 1,
+        unit: 'px',
+        x: imageRef.width / 2 - size / 2,
+        y: imageRef.height / 2 - size / 2,
+        width: size,
+        height: size,
+      });
+    }
+  }, [imageRef]);
+
+  useEffect(() => {
+    document.body.style.position = 'static';
+    reset();
+  }, [reset]);
+
+  const onImageLoaded = (image) => {
+    setImageRef(image);
+    reset();
+    return false;
+  };
+
+
+  const onCropChange = (crop) => {
+    setCropOtions(crop);
+  };
+
+  const getCroppedImg = async(image, crop) => {
+    const canvas = document.createElement('canvas');
+    const scaleX = image.naturalWidth / image.width;
+    const scaleY = image.naturalHeight / image.height;
+    canvas.width = crop.width;
+    canvas.height = crop.height;
+    const ctx = canvas.getContext('2d');
+    ctx?.drawImage(image, crop.x * scaleX, crop.y * scaleY, crop.width * scaleX, crop.height * scaleY, 0, 0, crop.width, crop.height);
+    try {
+      const blob = await canvasToBlob(canvas);
+      return blob;
+    }
+    catch (err) {
+      logger.error(err);
+      toastError(new Error('Failed to draw image'));
+    }
+  };
+
+  const crop = async() => {
+    // crop immages
+    if (imageRef && cropOptions?.width && cropOptions.height) {
+      const result = await getCroppedImg(imageRef, cropOptions);
+      onCropCompleted(result);
+    }
+  };
+
+  return (
+    <Modal isOpen={isShow} toggle={onModalClose}>
+      <ModalHeader tag="h4" toggle={onModalClose} className="bg-info text-light">
+        {t('crop_image_modal.image_crop')}
+      </ModalHeader>
+      <ModalBody className="my-4">
+        <ReactCrop src={src} crop={cropOptions} onImageLoaded={onImageLoaded} onChange={onCropChange} circularCrop={isCircular} />
+      </ModalBody>
+      <ModalFooter>
+        <button type="button" className="btn btn-outline-danger rounded-pill mr-auto" onClick={reset}>
+          {t('crop_image_modal.reset')}
+        </button>
+        <button type="button" className="btn btn-outline-secondary rounded-pill mr-2" onClick={onModalClose}>
+          {t('crop_image_modal.cancel')}
+        </button>
+        <button type="button" className="btn btn-outline-primary rounded-pill" onClick={crop}>
+          {t('crop_image_modal.crop')}
+        </button>
+      </ModalFooter>
+    </Modal>
+  );
+};
+
+export default ImageCropModal;

+ 30 - 7
packages/app/src/components/Drawio.tsx

@@ -1,5 +1,5 @@
 import React, {
-  useCallback, useEffect, useMemo, useRef,
+  useCallback, useEffect, useMemo, useRef, useState,
 } from 'react';
 
 import EventEmitter from 'events';
@@ -8,35 +8,55 @@ import { useTranslation } from 'next-i18next';
 import { debounce } from 'throttle-debounce';
 
 import { CustomWindow } from '~/interfaces/global';
-import { IGraphViewer } from '~/interfaces/graph-viewer';
+import { IGraphViewer, isGraphViewer } from '~/interfaces/graph-viewer';
 
 import NotAvailableForGuest from './NotAvailableForGuest';
 
 type Props = {
+  GraphViewer: IGraphViewer,
   drawioContent: string,
   rangeLineNumberOfMarkdown: { beginLineNumber: number, endLineNumber: number },
   isPreview?: boolean,
 }
 
+// It calls callback when GraphViewer is not null.
+// eslint-disable-next-line @typescript-eslint/ban-types
+const waitForGraphViewer = async(callback: Function) => {
+  const MAX_WAIT_COUNT = 10; // no reason for 10
+
+  for (let i = 0; i < MAX_WAIT_COUNT; i++) {
+    if (isGraphViewer((window as CustomWindow).GraphViewer)) {
+      callback((window as CustomWindow).GraphViewer);
+      break;
+    }
+    // Sleep 500 ms
+    // eslint-disable-next-line no-await-in-loop
+    await new Promise<void>(r => setTimeout(() => r(), 500));
+  }
+};
+
 const Drawio = (props: Props): JSX.Element => {
 
   const { t } = useTranslation();
 
+  // Wrap with a function since GraphViewer is a function.
+  // This applies when call setGraphViewer as well.
+  const [GraphViewer, setGraphViewer] = useState<IGraphViewer | undefined>(() => (window as CustomWindow).GraphViewer);
+
   const { drawioContent, rangeLineNumberOfMarkdown, isPreview } = props;
 
   // const { open: openDrawioModal } = useDrawioModalForPage();
 
   const drawioContainerRef = useRef<HTMLDivElement>(null);
 
-  const globalEmitter: EventEmitter = useMemo(() => (window as CustomWindow).globalEmitter, []);
-  const GraphViewer: IGraphViewer = useMemo(() => (window as CustomWindow).GraphViewer, []);
+  const globalEmitter: EventEmitter = (window as CustomWindow).globalEmitter;
 
   const editButtonClickHandler = useCallback(() => {
     const { beginLineNumber, endLineNumber } = rangeLineNumberOfMarkdown;
     globalEmitter.emit('launchDrawioModal', beginLineNumber, endLineNumber);
   }, [rangeLineNumberOfMarkdown, globalEmitter]);
 
-  const renderDrawio = useCallback(() => {
+  const renderDrawio = useCallback((GraphViewer: IGraphViewer) => {
     if (drawioContainerRef.current == null) {
       return;
     }
@@ -51,16 +71,19 @@ const Drawio = (props: Props): JSX.Element => {
         GraphViewer.createViewerForElement(div);
       }
     }
-  }, [GraphViewer]);
+  }, [drawioContainerRef]);
 
   const renderDrawioWithDebounce = useMemo(() => debounce(200, renderDrawio), [renderDrawio]);
 
   useEffect(() => {
     if (GraphViewer == null) {
+      waitForGraphViewer((gv: IGraphViewer) => {
+        setGraphViewer(() => gv);
+      });
       return;
     }
 
-    renderDrawioWithDebounce();
+    renderDrawioWithDebounce(GraphViewer);
   }, [renderDrawioWithDebounce, GraphViewer]);
 
   return (

+ 23 - 23
packages/app/src/components/InstallerForm.jsx

@@ -1,10 +1,10 @@
 import React from 'react';
 
 import i18next from 'i18next';
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
-import { localeMetadatas } from '~/client/util/i18n';
+// import { localeMetadatas } from '~/client/util/i18n';
 import { useCsrfToken } from '~/stores/context';
 
 class InstallerForm extends React.Component {
@@ -17,31 +17,31 @@ class InstallerForm extends React.Component {
       isSubmittingDisabled: false,
       selectedLang: {},
     };
-    // this.checkUserName = this.checkUserName.bind(this);
+    this.checkUserName = this.checkUserName.bind(this);
 
     this.submitHandler = this.submitHandler.bind(this);
   }
 
-  UNSAFE_componentWillMount() {
-    const meta = localeMetadatas.find(v => v.id === i18next.language);
-    if (meta == null) {
-      return this.setState({ selectedLang: localeMetadatas[0] });
-    }
-    this.setState({ selectedLang: meta });
-  }
-
-  // checkUserName(event) {
-  //   const axios = require('axios').create({
-  //     headers: {
-  //       'Content-Type': 'application/json',
-  //       'X-Requested-With': 'XMLHttpRequest',
-  //     },
-  //     responseType: 'json',
-  //   });
-  //   axios.get('/_api/v3/check-username', { params: { username: event.target.value } })
-  //     .then((res) => { return this.setState({ isValidUserName: res.data.valid }) });
+  // UNSAFE_componentWillMount() {
+  //   const meta = localeMetadatas.find(v => v.id === i18next.language);
+  //   if (meta == null) {
+  //     return this.setState({ selectedLang: localeMetadatas[0] });
+  //   }
+  //   this.setState({ selectedLang: meta });
   // }
 
+  checkUserName(event) {
+    const axios = require('axios').create({
+      headers: {
+        'Content-Type': 'application/json',
+        'X-Requested-With': 'XMLHttpRequest',
+      },
+      responseType: 'json',
+    });
+    axios.get('/_api/v3/check-username', { params: { username: event.target.value } })
+      .then((res) => { return this.setState({ isValidUserName: res.data.valid }) });
+  }
+
   changeLanguage(meta) {
     i18next.changeLanguage(meta.id);
     this.setState({ selectedLang: meta });
@@ -97,7 +97,7 @@ class InstallerForm extends React.Component {
                   value={this.state.selectedLang.id}
                   name="registerForm[app:globalLang]"
                 />
-                <div className="dropdown-menu" aria-labelledby="dropdownLanguage">
+                {/* <div className="dropdown-menu" aria-labelledby="dropdownLanguage">
                   {
                     localeMetadatas.map(meta => (
                       <button
@@ -111,7 +111,7 @@ class InstallerForm extends React.Component {
                       </button>
                     ))
                   }
-                </div>
+                </div> */}
               </div>
             </div>
 

+ 14 - 18
packages/app/src/styles/_admin.scss → packages/app/src/components/Layout/Admin.module.scss

@@ -1,14 +1,17 @@
+@use '~/styles/bootstrap/init' as *;
+@use '~/styles/mixins';
+
 $slack-work-space-name-card-background: #fff5ff;
 $slack-work-space-name-card-border: #efc1f6;
 
-.admin-page {
+.admin-page :global {
   .title {
     padding-top: 1rem;
     padding-bottom: 1rem;
 
     line-height: 1em;
 
-    @include variable-font-size(28px);
+    @include mixins.variable-font-size(28px);
     line-height: 1.1em;
   }
 
@@ -28,8 +31,6 @@ $slack-work-space-name-card-border: #efc1f6;
   }
 
   .admin-customize {
-    @import 'hljs';
-
     .ss-container img {
       padding: 0.5em;
       background-color: $gray-300;
@@ -169,15 +170,8 @@ $slack-work-space-name-card-border: #efc1f6;
 
     // switch layout for Bridge component
     .grw-bridge-container {
-      .label {
-        @extend .mt-5;
-      }
-
       // with ProxyCircle
       &.with-proxy {
-        .label {
-          @extend .mt-0;
-        }
         .hr-container {
           margin-top: 40px;
           @include media-breakpoint-up(lg) {
@@ -293,13 +287,15 @@ $slack-work-space-name-card-border: #efc1f6;
       background-color: rgba($info, 0.1);
     }
   }
-}
 
-.admin-navigation {
-  & > a + a {
-    margin-top: 2px;
-  }
-  &.sticky-top {
-    top: 30px;
+  .admin-navigation {
+    & > a + a {
+      margin-top: 2px;
+    }
+    &.sticky-top {
+      top: 30px;
+    }
   }
 }
+
+

+ 9 - 2
packages/app/src/components/AdminLayout.tsx → packages/app/src/components/Layout/AdminLayout.tsx

@@ -3,9 +3,13 @@ import React, { ReactNode } from 'react';
 import dynamic from 'next/dynamic';
 import { Provider } from 'unstated';
 
-import { GrowiNavbar } from './Navbar/GrowiNavbar';
+import { GrowiNavbar } from '../Navbar/GrowiNavbar';
+
 import { RawLayout } from './RawLayout';
 
+import styles from './Admin.module.scss';
+
+
 // import { injectableContainers } from '~/client/admin';
 
 type Props = {
@@ -25,9 +29,10 @@ const AdminLayout = ({
 }: Props): JSX.Element => {
 
   const AdminNavigation = dynamic(() => import('~/components/Admin/Common/AdminNavigation'), { ssr: false });
+  const SystemVersion = dynamic(() => import('../SystemVersion'), { ssr: false });
 
   return (
-    <RawLayout title={title}>
+    <RawLayout title={title} className={`admin-page ${styles['admin-page']}`}>
       <GrowiNavbar />
 
       <header className="py-0">
@@ -48,6 +53,8 @@ const AdminLayout = ({
           </div>
         </div>
       </div>
+
+      <SystemVersion />
     </RawLayout>
   );
 };

+ 58 - 0
packages/app/src/components/Layout/BasicLayout.tsx

@@ -0,0 +1,58 @@
+import React, { ReactNode } from 'react';
+
+import dynamic from 'next/dynamic';
+
+import { GrowiNavbar } from '../Navbar/GrowiNavbar';
+import Sidebar from '../Sidebar';
+
+import { RawLayout } from './RawLayout';
+
+
+type Props = {
+  title: string
+  className?: string,
+  children?: ReactNode
+}
+
+export const BasicLayout = ({ children, title, className }: Props): JSX.Element => {
+
+  // const HotkeysManager = dynamic(() => import('../client/js/components/Hotkeys/HotkeysManager'), { ssr: false });
+  // const PageCreateModal = dynamic(() => import('../client/js/components/PageCreateModal'), { ssr: false });
+  const GrowiNavbarBottom = dynamic(() => import('../Navbar/GrowiNavbarBottom').then(mod => mod.GrowiNavbarBottom), { ssr: false });
+  const ShortcutsModal = dynamic(() => import('../ShortcutsModal'), { ssr: false });
+  const SystemVersion = dynamic(() => import('../SystemVersion'), { ssr: false });
+  // Page modals
+  const PageCreateModal = dynamic(() => import('../PageCreateModal'), { ssr: false });
+  const PageDuplicateModal = dynamic(() => import('../PageDuplicateModal'), { ssr: false });
+  const PageDeleteModal = dynamic(() => import('../PageDeleteModal'), { ssr: false });
+  const PageRenameModal = dynamic(() => import('../PageRenameModal'), { ssr: false });
+  const PagePresentationModal = dynamic(() => import('../PagePresentationModal'), { ssr: false });
+
+  return (
+    <RawLayout title={title} className={className}>
+      <GrowiNavbar />
+
+      <div className="page-wrapper d-flex d-print-block">
+        <div className="grw-sidebar-wrapper">
+          <Sidebar />
+        </div>
+
+        <div className="flex-fill mw-0">
+          {children}
+        </div>
+      </div>
+
+      <GrowiNavbarBottom />
+
+      <PageCreateModal />
+      <PageDuplicateModal />
+      <PageDeleteModal />
+      <PageRenameModal />
+      <PagePresentationModal />
+      {/* <HotkeysManager /> */}
+
+      <ShortcutsModal />
+      <SystemVersion showShortcutsButton />
+    </RawLayout>
+  );
+};

+ 48 - 0
packages/app/src/components/Layout/RawLayout.tsx

@@ -0,0 +1,48 @@
+import React, { ReactNode, useEffect, useState } from 'react';
+
+import Head from 'next/head';
+
+import { useGrowiTheme } from '~/stores/context';
+import { Themes, useNextThemes } from '~/stores/use-next-themes';
+
+import { ThemeProvider } from '../Theme/utils/ThemeProvider';
+
+type Props = {
+  title: string,
+  className?: string,
+  children?: ReactNode,
+}
+
+export const RawLayout = ({ children, title, className }: Props): JSX.Element => {
+
+  const classNames: string[] = ['wrapper'];
+  if (className != null) {
+    classNames.push(className);
+  }
+  const { data: growiTheme } = useGrowiTheme();
+
+  // get color scheme from next-themes
+  const { resolvedTheme } = useNextThemes();
+
+  const [colorScheme, setColorScheme] = useState<Themes|undefined>(undefined);
+
+  // set colorScheme in CSR
+  useEffect(() => {
+    setColorScheme(resolvedTheme as Themes);
+  }, [resolvedTheme]);
+
+  return (
+    <>
+      <Head>
+        <title>{title}</title>
+        <meta charSet="utf-8" />
+        <meta name="viewport" content="initial-scale=1.0, width=device-width" />
+      </Head>
+      <ThemeProvider theme={growiTheme}>
+        <div className={classNames.join(' ')} data-color-scheme={colorScheme}>
+          {children}
+        </div>
+      </ThemeProvider>
+    </>
+  );
+};

+ 1 - 1
packages/app/src/components/LikeButtons.tsx

@@ -50,7 +50,7 @@ const LikeButtons: FC<LikeButtonsProps> = (props: LikeButtonsProps) => {
         type="button"
         id="like-button"
         onClick={onLikeClicked}
-        className={`shadow-none btn ${styles['btn-like']} border-0
+        className={`shadow-none btn btn-like ${styles['btn-like']} border-0
             ${isLiked ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`}
       >
         <i className={`fa ${isLiked ? 'fa-heart' : 'fa-heart-o'}`}></i>

+ 2 - 2
packages/app/src/components/LoginForm.jsx

@@ -1,8 +1,8 @@
 import React from 'react';
 
+import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import ReactCardFlip from 'react-card-flip';
-import { useTranslation } from 'next-i18next';
 
 import AppContainer from '~/client/services/AppContainer';
 import { useCsrfToken } from '~/stores/context';
@@ -309,7 +309,7 @@ class LoginForm extends React.Component {
                 {isRegistrationEnabled && (
                   <div className="text-right mb-2">
                     <a href="#register" id="register" className="link-switch" onClick={this.switchForm}>
-                      <i className="ti-check-box"></i> {t('Sign up is here')}
+                      <i className="ti ti-check-box"></i> {t('Sign up is here')}
                     </a>
                   </div>
                 )}

+ 1 - 1
packages/app/src/components/Me/DisassociateModal.tsx

@@ -57,7 +57,7 @@ const DisassociateModal = (props: Props): JSX.Element => {
           { t('Cancel') }
         </button>
         <button type="button" className="btn btn-sm btn-danger" onClick={disassociateAccountHandler}>
-          <i className="ti-unlink"></i>
+          <i className="ti ti-unlink"></i>
           { t('Disassociate') }
         </button>
       </ModalFooter>

+ 2 - 2
packages/app/src/components/Me/ExternalAccountRow.jsx

@@ -2,8 +2,8 @@
 import React from 'react';
 
 import dateFnsFormat from 'date-fns/format';
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 const ExternalAccountRow = (props) => {
   const { t } = useTranslation();
@@ -22,7 +22,7 @@ const ExternalAccountRow = (props) => {
           className="btn btn-sm btn-danger"
           onClick={() => props.openDisassociateModal(account)}
         >
-          <i className="ti-unlink"></i>
+          <i className="ti ti-unlink"></i>
           { t('Disassociate') }
         </button>
       </td>

+ 0 - 125
packages/app/src/components/Me/ImageCropModal.jsx

@@ -1,125 +0,0 @@
-import React from 'react';
-
-import canvasToBlob from 'async-canvas-to-blob';
-import PropTypes from 'prop-types';
-import { useTranslation } from 'next-i18next';
-import ReactCrop from 'react-image-crop';
-import {
-  Modal,
-  ModalHeader,
-  ModalBody,
-  ModalFooter,
-} from 'reactstrap';
-
-import 'react-image-crop/dist/ReactCrop.css';
-import { toastError } from '~/client/util/apiNotification';
-import loggerFactory from '~/utils/logger';
-
-
-const logger = loggerFactory('growi:ImageCropModal');
-
-class ImageCropModal extends React.Component {
-
-  // demo: https://codesandbox.io/s/72py4jlll6
-  constructor(props) {
-    super();
-    this.state = {
-      crop: null,
-      imageRef: null,
-    };
-    this.onImageLoaded = this.onImageLoaded.bind(this);
-    this.onCropChange = this.onCropChange.bind(this);
-    this.getCroppedImg = this.getCroppedImg.bind(this);
-    this.crop = this.crop.bind(this);
-    this.reset = this.reset.bind(this);
-    this.imageRef = null;
-  }
-
-  onImageLoaded(image) {
-    this.setState({ imageRef: image }, () => this.reset());
-    return false; // Return false when setting crop state in here.
-  }
-
-  onCropChange(crop) {
-    this.setState({ crop });
-  }
-
-  async getCroppedImg(image, crop, fileName) {
-    const canvas = document.createElement('canvas');
-    const scaleX = image.naturalWidth / image.width;
-    const scaleY = image.naturalHeight / image.height;
-    canvas.width = crop.width;
-    canvas.height = crop.height;
-    const ctx = canvas.getContext('2d');
-    ctx.drawImage(image, crop.x * scaleX, crop.y * scaleY, crop.width * scaleX, crop.height * scaleY, 0, 0, crop.width, crop.height);
-    try {
-      const blob = await canvasToBlob(canvas);
-      return blob;
-    }
-    catch (err) {
-      logger.error(err);
-      toastError(new Error('Failed to draw image'));
-    }
-  }
-
-  async crop() {
-    // crop immages
-    if (this.state.imageRef && this.state.crop.width && this.state.crop.height) {
-      const croppedImage = await this.getCroppedImg(this.state.imageRef, this.state.crop, '/images/icons/user');
-      this.props.onCropCompleted(croppedImage);
-    }
-  }
-
-  reset() {
-    const size = Math.min(this.state.imageRef.width, this.state.imageRef.height);
-    this.setState({
-      crop: {
-        aspect: 1,
-        unit: 'px',
-        x: this.state.imageRef.width / 2 - size / 2,
-        y: this.state.imageRef.height / 2 - size / 2,
-        width: size,
-        height: size,
-      },
-    });
-  }
-
-  render() {
-    return (
-      <Modal isOpen={this.props.show} toggle={this.props.onModalClose}>
-        <ModalHeader tag="h4" toggle={this.props.onModalClose} className="bg-info text-light">
-          Image Crop
-        </ModalHeader>
-        <ModalBody className="my-4">
-          <ReactCrop circularCrop src={this.props.src} crop={this.state.crop} onImageLoaded={this.onImageLoaded} onChange={this.onCropChange} />
-        </ModalBody>
-        <ModalFooter>
-          <button type="button" className="btn btn-outline-danger rounded-pill mr-auto" onClick={this.reset}>
-            Reset
-          </button>
-          <button type="button" className="btn btn-outline-secondary rounded-pill mr-2" onClick={this.props.onModalClose}>
-            Cancel
-          </button>
-          <button type="button" className="btn btn-outline-primary rounded-pill" onClick={this.crop}>
-            Crop
-          </button>
-        </ModalFooter>
-      </Modal>
-    );
-  }
-
-}
-
-ImageCropModal.propTypes = {
-  show: PropTypes.bool.isRequired,
-  src: PropTypes.string,
-  onModalClose: PropTypes.func.isRequired,
-  onCropCompleted: PropTypes.func.isRequired,
-};
-
-const ImageCropModalWrapperFC = (props) => {
-  const { t } = useTranslation();
-  return <ImageCropModal t={t} {...props} />;
-};
-
-export default ImageCropModalWrapperFC;

+ 3 - 2
packages/app/src/components/Me/ProfileImageSettings.tsx

@@ -6,10 +6,10 @@ import { useTranslation } from 'next-i18next';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiPost, apiPostForm } from '~/client/util/apiv1-client';
 import { apiv3Put } from '~/client/util/apiv3-client';
+import ImageCropModal from '~/components/Common/ImageCropModal';
 import { useCurrentUser } from '~/stores/context';
 import { generateGravatarSrc, GRAVATAR_DEFAULT } from '~/utils/gravatar';
 
-import ImageCropModal from './ImageCropModal';
 
 const DEFAULT_IMAGE = '/images/icons/user.svg';
 
@@ -155,10 +155,11 @@ const ProfileImageSettings = (): JSX.Element => {
       </div>
 
       <ImageCropModal
-        show={showImageCropModal}
+        isShow={showImageCropModal}
         src={imageCropSrc}
         onModalClose={() => setShowImageCropModal(false)}
         onCropCompleted={cropCompletedHandler}
+        isCircular
       />
 
       <div className="row my-3">

+ 12 - 28
packages/app/src/components/Navbar/AppearanceModeDropdown.tsx

@@ -1,5 +1,5 @@
 import React, {
-  FC, useState, useCallback, useRef,
+  FC, useCallback, useRef,
 } from 'react';
 
 import { useTranslation } from 'next-i18next';
@@ -7,15 +7,8 @@ import { useRipple } from 'react-use-ripple';
 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 { Themes, useNextThemes } from '~/stores/use-next-themes';
 
 import MoonIcon from '../Icons/MoonIcon';
 import SidebarDockIcon from '../Icons/SidebarDockIcon';
@@ -31,9 +24,9 @@ export const AppearanceModeDropdown:FC<AppearanceModeDropdownProps> = (props: Ap
 
   const { isAuthenticated } = props;
 
-  const [useOsSettings, setOsSettings] = useState(!isUserPreferenceExists());
-  const [isDarkMode, setIsDarkMode] = useState(isDarkModeByUtil());
-
+  const {
+    setTheme, resolvedTheme, useOsSettings, isDarkMode,
+  } = useNextThemes();
   const { data: isPreferDrawerMode, update: updatePreferDrawerMode } = usePreferDrawerModeByUser();
   const { data: isPreferDrawerModeOnEdit, mutate: mutatePreferDrawerModeOnEdit } = usePreferDrawerModeOnEditByUser();
   const { scheduleToPut } = useUserUISettings();
@@ -52,27 +45,18 @@ export const AppearanceModeDropdown:FC<AppearanceModeDropdownProps> = (props: Ap
     }
   }, [updatePreferDrawerMode, mutatePreferDrawerModeOnEdit, scheduleToPut]);
 
-  const followOsCheckboxModifiedHandler = useCallback((useOsSettings: boolean) => {
-    if (useOsSettings) {
-      removeUserPreference();
+  const followOsCheckboxModifiedHandler = useCallback((isChecked: boolean) => {
+    if (isChecked) {
+      setTheme(Themes.system);
     }
     else {
-      updateUserPreferenceWithOsSettings();
+      setTheme(resolvedTheme ?? Themes.light);
     }
-    applyColorScheme();
-
-    // update states
-    setOsSettings(useOsSettings);
-    setIsDarkMode(isDarkModeByUtil());
-  }, []);
+  }, [resolvedTheme, setTheme]);
 
   const userPreferenceSwitchModifiedHandler = useCallback((isDarkMode: boolean) => {
-    updateUserPreference(isDarkMode);
-    applyColorScheme();
-
-    // update state
-    setIsDarkMode(isDarkModeByUtil());
-  }, []);
+    setTheme(isDarkMode ? 'dark' : 'light');
+  }, [setTheme]);
 
   /* eslint-disable react/prop-types */
   const IconWithTooltip = ({

+ 2 - 4
packages/app/src/components/Navbar/GlobalSearch.tsx

@@ -5,8 +5,7 @@ import assert from 'assert';
 import { useTranslation } from 'next-i18next';
 
 import { IFocusable } from '~/client/interfaces/focusable';
-import { IPageWithMeta } from '~/interfaces/page';
-import { IPageSearchMeta } from '~/interfaces/search';
+import { IPageWithSearchMeta } from '~/interfaces/search';
 import {
   useCurrentPagePath, useIsSearchScopeChildrenAsDefault, useIsSearchServiceReachable,
 } from '~/stores/context';
@@ -14,7 +13,6 @@ import { useGlobalSearchFormRef } from '~/stores/ui';
 
 import SearchForm from '../SearchForm';
 
-
 import styles from './GlobalSearch.module.scss';
 
 
@@ -40,7 +38,7 @@ export const GlobalSearch = (props: GlobalSearchProps): JSX.Element => {
   const [isFocused, setFocused] = useState<boolean>(false);
 
 
-  const gotoPage = useCallback((data: IPageWithMeta<IPageSearchMeta>[]) => {
+  const gotoPage = useCallback((data: IPageWithSearchMeta[]) => {
     assert(data.length > 0);
 
     const page = data[0].data; // should be single page selected

+ 5 - 3
packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -1,7 +1,9 @@
 import React, { useState, useEffect, useCallback } from 'react';
 
+
 import { isPopulated } from '@growi/core';
 import { useTranslation } from 'next-i18next';
+import dynamic from 'next/dynamic';
 import { DropdownItem } from 'reactstrap';
 
 import { exportAsMarkdown } from '~/client/services/page-operation';
@@ -26,16 +28,14 @@ import {
   useIsAbleToShowPageEditorModeManager, useIsAbleToShowPageAuthors,
 } from '~/stores/ui';
 
-import { AdditionalMenuItemsRendererProps } from '../Common/Dropdown/PageItemControl';
 import CreateTemplateModal from '../CreateTemplateModal';
 import AttachmentIcon from '../Icons/AttachmentIcon';
 import HistoryIcon from '../Icons/HistoryIcon';
 import PresentationIcon from '../Icons/PresentationIcon';
 import ShareLinkIcon from '../Icons/ShareLinkIcon';
-
+import { Skelton } from '../Skelton';
 
 import { GrowiSubNavigation } from './GrowiSubNavigation';
-import PageEditorModeManager from './PageEditorModeManager';
 import { SubNavButtons } from './SubNavButtons';
 
 
@@ -151,6 +151,8 @@ type GrowiContextualSubNavigationProps = {
 
 const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps): JSX.Element => {
 
+  const PageEditorModeManager = dynamic(() => import('./PageEditorModeManager'), { ssr: false, loading: () => <Skelton width={208} height={32.49} /> });
+
   const { data: currentPage, mutate: mutateCurrentPage } = useSWRxCurrentPage();
   const path = currentPage?.path;
 

+ 5 - 13
packages/app/src/components/Navbar/GrowiNavbar.tsx

@@ -18,19 +18,12 @@ import { useIsDeviceSmallerThanMd } from '~/stores/ui';
 import { HasChildren } from '../../interfaces/common';
 import GrowiLogo from '../Icons/GrowiLogo';
 
+import { GlobalSearchProps } from './GlobalSearch';
 import PersonalDropdown from './PersonalDropdown';
 
 import styles from './GrowiNavbar.module.scss';
-import { GlobalSearchProps } from './GlobalSearch';
 
 
-const ShowSkeltonInSSR = memo(({ children }: HasChildren): JSX.Element => {
-  return isServer()
-    ? <></>
-    : <>{children}</>;
-});
-ShowSkeltonInSSR.displayName = 'ShowSkeltonInSSR';
-
 const NavbarRight = memo((): JSX.Element => {
   const { t } = useTranslation();
 
@@ -53,7 +46,7 @@ const NavbarRight = memo((): JSX.Element => {
     return (
       <>
         <li className="nav-item">
-          <ShowSkeltonInSSR><InAppNotificationDropdown /></ShowSkeltonInSSR>
+          <InAppNotificationDropdown />
         </li>
 
         <li className="nav-item d-none d-md-block">
@@ -70,11 +63,11 @@ const NavbarRight = memo((): JSX.Element => {
         </li>
 
         <li className="grw-apperance-mode-dropdown nav-item dropdown">
-          <ShowSkeltonInSSR><AppearanceModeDropdown isAuthenticated={isAuthenticated} /></ShowSkeltonInSSR>
+          <AppearanceModeDropdown isAuthenticated={isAuthenticated} />
         </li>
 
         <li className="grw-personal-dropdown nav-item dropdown dropdown-toggle dropdown-toggle-no-caret" data-testid="grw-personal-dropdown">
-          <ShowSkeltonInSSR><PersonalDropdown /></ShowSkeltonInSSR>
+          <PersonalDropdown />
         </li>
       </>
     );
@@ -84,7 +77,7 @@ const NavbarRight = memo((): JSX.Element => {
     return (
       <>
         <li className="grw-apperance-mode-dropdown nav-item dropdown">
-          <ShowSkeltonInSSR><AppearanceModeDropdown isAuthenticated={isAuthenticated} /></ShowSkeltonInSSR>
+          <AppearanceModeDropdown isAuthenticated={isAuthenticated} />
         </li>
 
         <li id="login-user" className="nav-item"><a className="nav-link" href="/login">Login</a></li>;
@@ -128,7 +121,6 @@ const Confidential: FC<ConfidentialProps> = memo((props: ConfidentialProps): JSX
 });
 Confidential.displayName = 'Confidential';
 
-
 export const GrowiNavbar = (): JSX.Element => {
 
   const GlobalSearch = dynamic<GlobalSearchProps>(() => import('./GlobalSearch').then(mod => mod.GlobalSearch), { ssr: false });

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