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

Merge branch 'support/apply-nextjs-2' into imprv/create-new-pages

Yuki Takei 3 лет назад
Родитель
Сommit
7c5f43a14f
100 измененных файлов с 1158 добавлено и 1218 удалено
  1. 10 0
      .eslintrc.js
  2. 38 33
      .github/workflows/ci-app-prod.yml
  3. 5 4
      .github/workflows/ci-app.yml
  4. 8 4
      .github/workflows/reusable-app-prod.yml
  5. 32 1
      CHANGELOG.md
  6. 1 1
      lerna.json
  7. 3 3
      package.json
  8. 0 3
      packages/app-next/.eslintrc.json
  9. 0 35
      packages/app-next/.gitignore
  10. 0 20
      packages/app-next/tsconfig.json
  11. 4 0
      packages/app/.eslintignore
  12. 11 1
      packages/app/.eslintrc.js
  13. 4 1
      packages/app/.gitignore
  14. 0 0
      packages/app/_obsolete/config/webpack.common.js
  15. 0 0
      packages/app/_obsolete/config/webpack.dev.dll.js
  16. 0 0
      packages/app/_obsolete/config/webpack.dev.js
  17. 0 0
      packages/app/_obsolete/config/webpack.prod.js
  18. 6 6
      packages/app/_obsolete/src/client/admin.jsx
  19. 2 2
      packages/app/_obsolete/src/client/app.jsx
  20. 0 0
      packages/app/_obsolete/src/client/base.jsx
  21. 0 0
      packages/app/_obsolete/src/client/installer.jsx
  22. 0 0
      packages/app/_obsolete/src/client/nologin.jsx
  23. 6 1
      packages/app/config/migrate-mongo-config.js
  24. 0 0
      packages/app/config/next-i18next.config.ts
  25. 1 1
      packages/app/docker/Dockerfile
  26. 2 2
      packages/app/docker/README.md
  27. 97 70
      packages/app/next.config.js
  28. 30 22
      packages/app/package.json
  29. 10 1
      packages/app/public/static/locales/en_US/admin.json
  30. 8 0
      packages/app/public/static/locales/en_US/translation.json
  31. 10 1
      packages/app/public/static/locales/ja_JP/admin.json
  32. 8 0
      packages/app/public/static/locales/ja_JP/translation.json
  33. 10 1
      packages/app/public/static/locales/zh_CN/admin.json
  34. 8 0
      packages/app/public/static/locales/zh_CN/translation.json
  35. 0 7
      packages/app/resource/cdn-manifests.js
  36. 1 1
      packages/app/resource/locales/en_US/welcome.md
  37. 1 1
      packages/app/resource/locales/ja_JP/welcome.md
  38. 1 1
      packages/app/resource/locales/zh_CN/welcome.md
  39. 3 0
      packages/app/src/client/interfaces/clearable.ts
  40. 1 5
      packages/app/src/client/services/AdminAppContainer.js
  41. 1 7
      packages/app/src/client/services/AdminBasicSecurityContainer.js
  42. 0 54
      packages/app/src/client/services/AdminCustomizeContainer.js
  43. 1 6
      packages/app/src/client/services/AdminGeneralSecurityContainer.js
  44. 0 1
      packages/app/src/client/services/AdminGitHubSecurityContainer.js
  45. 0 1
      packages/app/src/client/services/AdminGoogleSecurityContainer.js
  46. 1 4
      packages/app/src/client/services/AdminImportContainer.js
  47. 1 4
      packages/app/src/client/services/AdminLdapSecurityContainer.js
  48. 1 4
      packages/app/src/client/services/AdminMarkDownContainer.js
  49. 1 5
      packages/app/src/client/services/AdminOidcSecurityContainer.js
  50. 1 5
      packages/app/src/client/services/AdminSamlSecurityContainer.js
  51. 1 3
      packages/app/src/client/services/AdminSlackIntegrationLegacyContainer.js
  52. 1 5
      packages/app/src/client/services/AdminTwitterSecurityContainer.js
  53. 1 0
      packages/app/src/client/services/PageContainer.js
  54. 1 1
      packages/app/src/client/util/apiv3-client.ts
  55. 1 1
      packages/app/src/client/util/i18n.js
  56. 2 1
      packages/app/src/components/Admin/App/AppSetting.jsx
  57. 0 51
      packages/app/src/components/Admin/App/AppSettingsPage.jsx
  58. 22 1
      packages/app/src/components/Admin/App/AppSettingsPageContents.tsx
  59. 1 1
      packages/app/src/components/Admin/App/FileUploadSetting.tsx
  60. 1 2
      packages/app/src/components/Admin/App/SesSetting.tsx
  61. 1 2
      packages/app/src/components/Admin/App/SmtpSetting.tsx
  62. 11 2
      packages/app/src/components/Admin/AuditLog/ActivityTable.tsx
  63. 18 3
      packages/app/src/components/Admin/AuditLog/SearchUsernameTypeahead.tsx
  64. 32 8
      packages/app/src/components/Admin/AuditLogManagement.tsx
  65. 0 56
      packages/app/src/components/Admin/CustomCssEditor.jsx
  66. 0 57
      packages/app/src/components/Admin/CustomHeaderEditor.jsx
  67. 0 56
      packages/app/src/components/Admin/CustomScriptEditor.jsx
  68. 22 25
      packages/app/src/components/Admin/Customize/Customize.jsx
  69. 5 6
      packages/app/src/components/Admin/Customize/CustomizeCssSetting.tsx
  70. 1 3
      packages/app/src/components/Admin/Customize/CustomizeFunctionSetting.tsx
  71. 4 3
      packages/app/src/components/Admin/Customize/CustomizeHeaderSetting.tsx
  72. 1 1
      packages/app/src/components/Admin/Customize/CustomizeLayoutSetting.tsx
  73. 185 0
      packages/app/src/components/Admin/Customize/CustomizeLogoSetting.tsx
  74. 4 3
      packages/app/src/components/Admin/Customize/CustomizeScriptSetting.tsx
  75. 2 2
      packages/app/src/components/Admin/Customize/CustomizeSidebarSetting.tsx
  76. 8 4
      packages/app/src/components/Admin/Customize/CustomizeThemeOptions.jsx
  77. 22 22
      packages/app/src/components/Admin/Customize/CustomizeThemeSetting.tsx
  78. 6 6
      packages/app/src/components/Admin/Customize/CustomizeTitle.jsx
  79. 1 0
      packages/app/src/components/Admin/Customize/ThemeColorBox.jsx
  80. 0 245
      packages/app/src/components/Admin/ElasticsearchManagement/ElasticsearchManagement.jsx
  81. 214 0
      packages/app/src/components/Admin/ElasticsearchManagement/ElasticsearchManagement.tsx
  82. 22 26
      packages/app/src/components/Admin/ElasticsearchManagement/RebuildIndexControls.jsx
  83. 4 2
      packages/app/src/components/Admin/ExportArchiveData/SelectCollectionsModal.jsx
  84. 1 1
      packages/app/src/components/Admin/ExportArchiveDataPage.jsx
  85. 23 1
      packages/app/src/components/Admin/ImportData/ImportDataPageContents.jsx
  86. 0 52
      packages/app/src/components/Admin/ImportDataPage.jsx
  87. 25 28
      packages/app/src/components/Admin/LegacySlackIntegration/LegacySlackIntegration.jsx
  88. 1 1
      packages/app/src/components/Admin/LegacySlackIntegration/SlackConfiguration.jsx
  89. 0 48
      packages/app/src/components/Admin/MarkdownSetting/MarkDownSetting.jsx
  90. 36 3
      packages/app/src/components/Admin/MarkdownSetting/MarkDownSettingContents.tsx
  91. 2 2
      packages/app/src/components/Admin/Notification/ManageGlobalNotification.jsx
  92. 22 31
      packages/app/src/components/Admin/Security/BasicSecuritySetting.jsx
  93. 2 5
      packages/app/src/components/Admin/Security/FacebookSecuritySetting.jsx
  94. 22 26
      packages/app/src/components/Admin/Security/GitHubSecuritySetting.jsx
  95. 13 6
      packages/app/src/components/Admin/Security/GitHubSecuritySettingContents.jsx
  96. 23 26
      packages/app/src/components/Admin/Security/GoogleSecuritySetting.jsx
  97. 14 8
      packages/app/src/components/Admin/Security/GoogleSecuritySettingContents.jsx
  98. 21 25
      packages/app/src/components/Admin/Security/LdapSecuritySetting.jsx
  99. 22 26
      packages/app/src/components/Admin/Security/LocalSecuritySetting.jsx
  100. 9 12
      packages/app/src/components/Admin/Security/LocalSecuritySettingContents.jsx

+ 10 - 0
.eslintrc.js

@@ -56,6 +56,7 @@ module.exports = {
       },
     ],
     '@typescript-eslint/no-explicit-any': 'off',
+    '@typescript-eslint/explicit-module-boundary-types': 'off',
     indent: [
       'error',
       2,
@@ -80,4 +81,13 @@ module.exports = {
       },
     ]],
   },
+  overrides: [
+    {
+      // enable the rule specifically for TypeScript files
+      files: ['*.ts', '*.tsx'],
+      rules: {
+        '@typescript-eslint/explicit-module-boundary-types': ['error'],
+      },
+    },
+  ],
 };

+ 38 - 33
.github/workflows/ci-app-prod.yml

@@ -3,7 +3,25 @@ name: Node CI for app production
 on:
   push:
     branches:
-      - master
+      # - master
+      - support/apply-nextjs-2
+    paths:
+      - .github/workflows/ci-app-prod.yml
+      - .github/workflows/reusable-app-prod.yml
+      - .github/workflows/reusable-app-reg-suit.yml
+      - tsconfig.base.json
+      - yarn.lock
+      - packages/app/**
+      - '!packages/app/docker/**'
+      - packages/core/**
+      - packages/slack/**
+      - packages/ui/**
+      - packages/plugin-**
+  pull_request:
+    branches:
+      # - master
+      - support/apply-nextjs-2
+    types: [opened, reopened, synchronize]
     paths:
       - .github/workflows/ci-app-prod.yml
       - .github/workflows/reusable-app-prod.yml
@@ -16,27 +34,12 @@ on:
       - packages/slack/**
       - packages/ui/**
       - packages/plugin-**
-  # pull_request:
-  #   branches:
-  #       - master
-  #   types: [opened, reopened, synchronize]
-  #   paths:
-  #     - .github/workflows/ci-app-prod.yml
-  #     - .github/workflows/reusable-app-prod.yml
-  #     - .github/workflows/reusable-app-reg-suit.yml
-  #     - tsconfig.base.json
-  #     - yarn.lock
-  #     - packages/app/**
-  #     - '!packages/app/docker/**'
-  #     - packages/core/**
-  #     - packages/slack/**
-  #     - packages/ui/**
-  #     - packages/plugin-**
 
 jobs:
 
   test-prod-node14:
-    uses: weseek/growi/.github/workflows/reusable-app-prod.yml@master
+    # uses: weseek/growi/.github/workflows/reusable-app-prod.yml@support/master
+    uses: weseek/growi/.github/workflows/reusable-app-prod.yml@support/apply-nextjs-2
     with:
       node-version: 14.x
       skip-cypress: true
@@ -45,28 +48,30 @@ jobs:
 
 
   test-prod-node16:
-    uses: weseek/growi/.github/workflows/reusable-app-prod.yml@master
+    # uses: weseek/growi/.github/workflows/reusable-app-prod.yml@master
+    uses: weseek/growi/.github/workflows/reusable-app-prod.yml@support/apply-nextjs-2
     with:
       node-version: 16.x
-      skip-cypress: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) && contains( github.event.pull_request.labels.*.name, 'github_actions' ) }}
+      # skip-cypress: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) && contains( github.event.pull_request.labels.*.name, 'github_actions' ) }}
+      skip-cypress: true
       cypress-report-artifact-name: Cypress report
     secrets:
       SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
 
 
-  run-reg-suit-node16:
-    needs: [test-prod-node16]
+  # run-reg-suit-node16:
+  #   needs: [test-prod-node16]
 
-    uses: weseek/growi/.github/workflows/reusable-app-reg-suit.yml@master
+  #   uses: weseek/growi/.github/workflows/reusable-app-reg-suit.yml@master
 
-    if: always()
+  #   if: always()
 
-    with:
-      node-version: 16.x
-      skip-reg-suit: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) && contains( github.event.pull_request.labels.*.name, 'github_actions' ) }}
-      cypress-report-artifact-name: Cypress report
-    secrets:
-      REG_NOTIFY_GITHUB_PLUGIN_CLIENTID: ${{ secrets.REG_NOTIFY_GITHUB_PLUGIN_CLIENTID }}
-      AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
-      AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
-      SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
+  #   with:
+  #     node-version: 16.x
+  #     skip-reg-suit: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) && contains( github.event.pull_request.labels.*.name, 'github_actions' ) }}
+  #     cypress-report-artifact-name: Cypress report
+  #   secrets:
+  #     REG_NOTIFY_GITHUB_PLUGIN_CLIENTID: ${{ secrets.REG_NOTIFY_GITHUB_PLUGIN_CLIENTID }}
+  #     AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
+  #     AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+  #     SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

+ 5 - 4
.github/workflows/ci-app.yml

@@ -150,16 +150,17 @@ jobs:
           cache: 'yarn'
           cache-dependency-path: '**/yarn.lock'
 
-      - name: Cache/Restore node_modules
+      - name: Cache/Restore node_modules and next cache files
         id: cache-dependencies
         uses: actions/cache@v3
         with:
           path: |
             **/node_modules
-          key: node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('packages/app/package.json') }}
+            ${{ github.workspace }}/packages/app/.next/cache
+          key: dev-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('packages/app/package.json') }}
           restore-keys: |
-            node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}-
-            node_modules-${{ runner.OS }}-node${{ matrix.node-version }}-
+            dev-${{ runner.OS }}-node${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }}-
+            dev-${{ runner.OS }}-node${{ matrix.node-version }}-
 
       - name: lerna bootstrap
         run: |

+ 8 - 4
.github/workflows/reusable-app-prod.yml

@@ -49,6 +49,7 @@ jobs:
     - name: Remove unnecessary packages
       run: |
         rm -rf packages/slackbot-proxy
+        rm -f "node_modules/@growi/slackbot-proxy"
 
     - name: Build
       run: |
@@ -59,17 +60,17 @@ jobs:
     - name: Archive production files
       id: archive-prod-files
       run: |
-        tar -cf production.tar \
+        tar -zcf production.tar.gz \
           package.json \
+          packages/app/.next \
           packages/app/config \
           packages/app/public \
           packages/app/resource \
           packages/app/tmp \
-          packages/app/migrate-mongo-config.js \
           packages/app/.env.production* \
           packages/*/package.json \
           packages/*/dist
-        echo ::set-output name=file::production.tar
+        echo ::set-output name=file::production.tar.gz
 
     - name: Upload production files as artifact
       uses: actions/upload-artifact@v3
@@ -81,7 +82,9 @@ jobs:
       uses: actions/upload-artifact@v3
       with:
         name: Bundle Analyzing Report
-        path: packages/app/report/bundle-analyzer.html
+        path: |
+          packages/app/.next/analyze/client.html
+          packages/app/.next/analyze/server.html
 
     - name: Slack Notification
       uses: weseek/ghaction-slack-notification@master
@@ -143,6 +146,7 @@ jobs:
     - name: Remove unnecessary packages
       run: |
         rm -rf packages/slackbot-proxy
+        rm -f "node_modules/@growi/slackbot-proxy"
 
     - name: lerna bootstrap --production
       run: |

+ 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/*"
   ]

+ 3 - 3
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",
@@ -82,9 +82,9 @@
     "stylelint": "^14.2.0",
     "stylelint-config-recess-order": "^3.0.0",
     "ts-jest": "^27.0.4",
-    "ts-node": "^9.1.1",
+    "ts-node": "^10.9.1",
     "tsconfig-paths": "^3.9.0",
-    "typescript": "^4.7.3",
+    "typescript": "~4.7",
     "yargs": "^17.3.1"
   },
   "engines": {

+ 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"]
-}

+ 4 - 0
packages/app/.eslintignore

@@ -1,7 +1,11 @@
+/_obsolete/**
 /dist/**
+/transpiled/**
 /public/**
+/config/next-i18next.config.js
 /src/client/legacy/thirdparty-js/**
 /src/client/util/reveal/plugins/markdown.js
 /src/linter-checker/**
+/src/utils/next.config.utils.js
 /tmp/**
 /next-env.d.ts

+ 11 - 1
packages/app/.eslintrc.js

@@ -38,9 +38,19 @@ module.exports = {
     '@typescript-eslint/no-var-requires': 'off',
 
     // set 'warn' temporarily -- 2021.08.02 Yuki Takei
-    '@typescript-eslint/explicit-module-boundary-types': ['warn'],
     '@typescript-eslint/no-use-before-define': ['warn'],
     '@typescript-eslint/no-this-alias': ['warn'],
     'jest/no-done-callback': ['warn'],
   },
+  overrides: [
+    {
+      // enable the rule specifically for TypeScript files
+      files: ['*.ts', '*.tsx'],
+      rules: {
+        // '@typescript-eslint/explicit-module-boundary-types': ['error'],
+        // set 'warn' temporarily -- 2022.07.25 Yuki Takei
+        '@typescript-eslint/explicit-module-boundary-types': ['warn'],
+      },
+    },
+  ],
 };

+ 4 - 1
packages/app/.gitignore

@@ -11,12 +11,15 @@ test/cypress/videos
 /build/
 /dist/
 /transpiled/
-/report/
 /public/static/js
 /public/static/styles
 /public/uploads
 /tmp/
 
+# transpiled configuration files for production build
+/config/next-i18next.config.js
+/src/utils/next.config.utils.js
+
 # dist (for GROWI v4.x and below)
 /public/*.chunk.js
 /public/*.chunk.js.LICENSE.txt

+ 0 - 0
packages/app/config/webpack.common.js → packages/app/_obsolete/config/webpack.common.js


+ 0 - 0
packages/app/config/webpack.dev.dll.js → packages/app/_obsolete/config/webpack.dev.dll.js


+ 0 - 0
packages/app/config/webpack.dev.js → packages/app/_obsolete/config/webpack.dev.js


+ 0 - 0
packages/app/config/webpack.prod.js → packages/app/_obsolete/config/webpack.prod.js


+ 6 - 6
packages/app/src/client/admin.jsx → packages/app/_obsolete/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 />,

+ 2 - 2
packages/app/src/client/app.jsx → packages/app/_obsolete/src/client/app.jsx

@@ -30,12 +30,12 @@ import { Page } from '../components/Page';
 import DisplaySwitcher from '../components/Page/DisplaySwitcher';
 import RedirectedAlert from '../components/Page/RedirectedAlert';
 import ShareLinkAlert from '../components/Page/ShareLinkAlert';
-import PageComment from '../components/PageComment';
+import { PageComment } from '../components/PageComment';
 import CommentEditorLazyRenderer from '../components/PageComment/CommentEditorLazyRenderer';
 import PageContentFooter from '../components/PageContentFooter';
 import BookmarkList from '../components/PageList/BookmarkList';
 import PageStatusAlert from '../components/PageStatusAlert';
-import PageTimeline from '../components/PageTimeline';
+import { PageTimeline } from '../components/PageTimeline';
 import RecentCreated from '../components/RecentCreated/RecentCreated';
 import { SearchPage } from '../components/SearchPage';
 import Sidebar from '../components/Sidebar';

+ 0 - 0
packages/app/src/client/base.jsx → packages/app/_obsolete/src/client/base.jsx


+ 0 - 0
packages/app/src/client/installer.jsx → packages/app/_obsolete/src/client/installer.jsx


+ 0 - 0
packages/app/src/client/nologin.jsx → packages/app/_obsolete/src/client/nologin.jsx


+ 6 - 1
packages/app/migrate-mongo-config.js → packages/app/config/migrate-mongo-config.js

@@ -4,16 +4,21 @@
  *
  * @author Yuki Takei <yuki@weseek.co.jp>
  */
+const isProduction = process.env.NODE_ENV === 'production';
 
 const { URL } = require('url');
 
+const { initMongooseGlobalSettings, getMongoUri, mongoOptions } = isProduction
+  // eslint-disable-next-line import/extensions, import/no-unresolved
+  ? require('../dist/server/util/mongoose-utils')
+  : require('../src/server/util/mongoose-utils');
+
 // get migrationsDir from env var
 const migrationsDir = process.env.MIGRATIONS_DIR;
 if (migrationsDir == null) {
   throw new Error('An env var MIGRATIONS_DIR must be set.');
 }
 
-const { initMongooseGlobalSettings, getMongoUri, mongoOptions } = require('@growi/core');
 
 initMongooseGlobalSettings();
 

+ 0 - 0
packages/app/src/next-i18next.config.ts → packages/app/config/next-i18next.config.ts


+ 1 - 1
packages/app/docker/Dockerfile

@@ -111,11 +111,11 @@ RUN yarn lerna run build
 # make artifacts
 RUN tar -cf packages.tar \
   package.json \
+  packages/app/.next \
   packages/app/config \
   packages/app/public \
   packages/app/resource \
   packages/app/tmp \
-  packages/app/migrate-mongo-config.js \
   packages/app/.env.production* \
   packages/*/package.json \
   packages/*/dist

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

+ 97 - 70
packages/app/next.config.js

@@ -1,29 +1,45 @@
-import eazyLogger from 'eazy-logger';
-import { I18NextHMRPlugin } from 'i18next-hmr/plugin';
-import { WebpackManifestPlugin } from 'webpack-manifest-plugin';
+/**
+ * == Notes for production build==
+ * The modules required from this file must be transpiled before running `next build`.
+ *
+ * See: https://github.com/vercel/next.js/discussions/35969#discussioncomment-2522954
+ */
 
-import { i18n, localePath } from './src/next-i18next.config';
-import { listScopedPackages, listPrefixedPackages } from './src/utils/next.config.utils';
+const { withSuperjson } = require('next-superjson');
+const { PHASE_PRODUCTION_BUILD, PHASE_PRODUCTION_SERVER } = require('next/constants');
 
 
-// setup logger
-const logger = eazyLogger.Logger({
-  prefix: '[{green:next.config.js}] ',
-  useLevelPrefixes: false,
-});
+// define additional entries
+const additionalWebpackEntries = {
+  boot: './src/client/boot',
+};
+
+
+const setupTranspileModules = () => {
+  const eazyLogger = require('eazy-logger');
+  const { listScopedPackages, listPrefixedPackages } = require('./src/utils/next.config.utils');
 
+  // setup logger
+  const logger = eazyLogger.Logger({
+    prefix: '[{green:next.config.js}] ',
+    useLevelPrefixes: false,
+  });
 
-const setupWithTM = () => {
   // define transpiled packages for '@growi/*'
   const packages = [
-    ...listScopedPackages(['@growi'], { ignorePackageNames: '@growi/app' }),
+    ...listScopedPackages(['@growi'], { ignorePackageNames: ['@growi/app'] }),
     // listing ESM packages until experimental.esmExternals works correctly to avoid ERR_REQUIRE_ESM
     'react-markdown',
     '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-']),
   ];
@@ -33,64 +49,75 @@ const setupWithTM = () => {
 
   return require('next-transpile-modules')(packages);
 };
-const withTM = setupWithTM();
 
 
-// define additional entries
-const additionalWebpackEntries = {
-  boot: './src/client/boot',
+module.exports = async(phase, { defaultConfig }) => {
+
+  const { i18n, localePath } = require('./config/next-i18next.config');
+
+  /** @type {import('next').NextConfig} */
+  const nextConfig = {
+    // == DOES NOT WORK
+    // see: https://github.com/vercel/next.js/discussions/27876
+    // experimental: { esmExternals: true }, // Prefer loading of ES Modules over CommonJS
+
+    reactStrictMode: true,
+    swcMinify: true,
+    typescript: {
+      tsconfigPath: 'tsconfig.build.client.json',
+    },
+    pageExtensions: ['page.tsx', 'page.ts', 'page.jsx', 'page.js'],
+
+    i18n,
+
+    /** @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) {
+        config.resolve.fallback.fs = false;
+      }
+
+      // See: https://webpack.js.org/configuration/externals/
+      // This provides a way of excluding dependencies from the output bundles
+      config.externals.push('dtrace-provider');
+      config.externals.push('mongoose');
+
+      // configure additional entries
+      const orgEntry = config.entry;
+      config.entry = () => {
+        return orgEntry().then((entry) => {
+          return { ...entry, ...additionalWebpackEntries };
+        });
+      };
+
+      const { WebpackManifestPlugin } = require('webpack-manifest-plugin');
+      config.plugins.push(
+        new WebpackManifestPlugin({
+          fileName: 'custom-manifest.json',
+        }),
+      );
+
+      // setup i18next-hmr
+      if (!options.isServer && options.dev) {
+        const { I18NextHMRPlugin } = require('i18next-hmr/plugin');
+        config.plugins.push(new I18NextHMRPlugin({ localesDir: localePath }));
+      }
+
+      return config;
+    },
+
+  };
+
+  // production server
+  if (phase === PHASE_PRODUCTION_SERVER) {
+    return withSuperjson()(nextConfig);
+  }
+
+  const withTM = setupTranspileModules();
+  const withBundleAnalyzer = require('@next/bundle-analyzer')({
+    enabled: phase === PHASE_PRODUCTION_BUILD || process.env.ANALYZE === 'true',
+  });
+
+  return withBundleAnalyzer(withTM(withSuperjson()(nextConfig)));
 };
-
-
-/** @type {import('next').NextConfig} */
-const nextConfig = {
-  // == DOES NOT WORK
-  // see: https://github.com/vercel/next.js/discussions/27876
-  // experimental: { esmExternals: true }, // Prefer loading of ES Modules over CommonJS
-
-  reactStrictMode: true,
-  typescript: {
-    tsconfigPath: 'tsconfig.build.client.json',
-  },
-  pageExtensions: ['page.tsx', 'page.ts', 'page.jsx', 'page.js'],
-
-  i18n,
-
-  /** @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) {
-      config.resolve.fallback.fs = false;
-    }
-
-    // See: https://webpack.js.org/configuration/externals/
-    // This provides a way of excluding dependencies from the output bundles
-    config.externals.push('dtrace-provider');
-
-    // configure additional entries
-    const orgEntry = config.entry;
-    config.entry = () => {
-      return orgEntry().then((entry) => {
-        return { ...entry, ...additionalWebpackEntries };
-      });
-    };
-
-    config.plugins.push(
-      new WebpackManifestPlugin({
-        fileName: 'custom-manifest.json',
-      }),
-    );
-
-    // setup i18next-hmr
-    if (!options.isServer && options.dev) {
-      config.plugins.push(new I18NextHMRPlugin({ localesDir: localePath }));
-    }
-
-    return config;
-  },
-
-};
-
-module.exports = withTM(nextConfig);

+ 30 - 22
packages/app/package.json

@@ -1,32 +1,31 @@
 {
   "name": "@growi/app",
-  "version": "5.1.0-RC.2",
+  "version": "5.1.1-RC.0",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
-    "build": "yarn next build",
+    "build": "run-p build:*",
     "start": "yarn next start",
-    "//// for production (obsolete)": "",
-    "start:obsolete": "yarn build && yarn server",
-    "build:obsolete": "run-p build:*",
-    "build:client": "yarn cross-env NODE_ENV=production webpack --config config/webpack.prod.js",
+    "build:client": "yarn next build",
+    "prebuild:client": "tsc -p tsconfig.build.next.config.json",
     "build:server": "yarn cross-env NODE_ENV=production tsc -p tsconfig.build.server.json && tsc-alias -p tsconfig.build.server.json",
+    "postbuild:server": "npx -y shx mv transpiled/src dist && npx -y shx cp -r transpiled/config/* config && npx -y shx cp -r src/server/views dist/server/ && npx -y shx rm -rf transpiled",
     "clean": "npx -y shx rm -rf dist transpiled",
     "prebuild": "yarn cross-env NODE_ENV=production run-p clean resources:*",
-    "postbuild": "npx -y shx mv transpiled/src dist && npx -y shx cp -r transpiled/config/* config && npx -y shx cp -r src/server/views dist/server/ && npx -y shx rm -rf transpiled",
     "server": "yarn cross-env NODE_ENV=production node -r dotenv-flow/config dist/server/app.js",
     "server:ci": "yarn server --ci",
     "preserver": "yarn cross-env NODE_ENV=production yarn migrate",
-    "migrate": "node -r dotenv-flow/config node_modules/.bin/migrate-mongo up",
+    "migrate": "node -r dotenv-flow/config node_modules/.bin/migrate-mongo up -f config/migrate-mongo-config.js",
     "//// for development": "",
     "dev": "yarn cross-env NODE_ENV=development ts-node-dev -r tsconfig-paths/register -r dotenv-flow/config --inspect --transpile-only src/server/app.ts",
     "predev": "yarn cross-env NODE_ENV=development run-p resources:* dev:migrate:up",
+    "dev:analyze": "yarn cross-env ANALYZE=true yarn dev",
     "dev:migrate-mongo": "yarn cross-env NODE_ENV=development yarn ts-node node_modules/.bin/migrate-mongo",
     "dev:migrate": "yarn dev:migrate:up",
-    "dev:migrate:create": "yarn dev:migrate-mongo create",
-    "dev:migrate:status": "yarn dev:migrate-mongo status",
-    "dev:migrate:up": "yarn dev:migrate-mongo up",
-    "dev:migrate:down": "yarn dev:migrate-mongo down",
+    "dev:migrate:create": "yarn dev:migrate-mongo create -f config/migrate-mongo-config.js",
+    "dev:migrate:status": "yarn dev:migrate-mongo status -f config/migrate-mongo-config.js",
+    "dev:migrate:up": "yarn dev:migrate-mongo up -f config/migrate-mongo-config.js",
+    "dev:migrate:down": "yarn dev:migrate-mongo down -f config/migrate-mongo-config.js",
     "cy:run": "cypress run --browser chrome",
     "//// for CI": "",
     "dev:ci": "yarn dev --ci",
@@ -63,11 +62,12 @@
     "@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/core": "^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",
@@ -82,6 +82,7 @@
     "axios-retry": "^3.2.4",
     "body-parser": "^1.18.2",
     "browser-bunyan": "^1.6.3",
+    "bson-objectid": "^2.0.3",
     "bunyan": "^1.8.15",
     "check-node-version": "^4.1.0",
     "compression": "^1.7.4",
@@ -126,8 +127,8 @@
     "multer-autoreap": "^1.0.3",
     "next": "^12.1.6",
     "next-i18next": "^11.0.0",
+    "next-superjson": "^0.0.4",
     "next-themes": "^0.2.0",
-    "next-transpile-modules": "^9.0.0",
     "nocache": "^3.0.1",
     "nodemailer": "^6.6.2",
     "nodemailer-ses-transport": "~1.5.0",
@@ -154,16 +155,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",
@@ -176,17 +179,21 @@
   },
   "// comments for defDependencies": {
     "@handsontable/react": "v3 requires handsontable >= 7.0.0.",
-    "handsontable": "v7.0.0 or above is no loger MIT lisence."
+    "handsontable": "v7.0.0 or above is no loger MIT lisence.",
+    "ts-node": "v10 occurs 'SyntaxError: Cannot use import statement outside a module' when using migrate-mongo"
   },
   "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",
+    "@icon/themify-icons": "1.0.1-alpha.3",
+    "@next/bundle-analyzer": "^12.2.3",
     "@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",
@@ -205,13 +212,13 @@
     "handsontable": "=6.2.2",
     "i18next-hmr": "^1.7.7",
     "jquery-slimscroll": "^1.3.8",
-    "jquery-ui": "^1.12.1",
     "jquery.cookie": "~1.4.1",
     "jshint": "^2.13.0",
     "load-css-file": "^1.0.0",
     "markdown-table": "^1.1.1",
     "material-icons": "^1.11.3",
     "morgan": "^1.10.0",
+    "next-transpile-modules": "^9.0.0",
     "normalize-path": "^3.0.0",
     "penpal": "^4.0.0",
     "plantuml-encoder": "^1.2.5",
@@ -237,6 +244,7 @@
     "swr": "^1.3.0",
     "throttle-debounce": "^3.0.1",
     "toastr": "^2.1.2",
+    "ts-node": "^9.1.1",
     "ts-node-dev": "^2.0.0",
     "tsc-alias": "^1.2.9",
     "unstated": "^2.1.1",

+ 10 - 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.",
@@ -523,6 +529,7 @@
     }
   },
   "audit_log_management": {
+    "user": "User",
     "username": "Username",
     "date": "Date",
     "action": "Action",
@@ -530,6 +537,8 @@
     "url": "URL",
     "settings": "Settings",
     "return": "Return",
+    "clear": "Clear search criteria",
+    "reload": "Reload",
     "activity_expiration_date": "Audit Log expiration date",
     "activity_expiration_date_explain": "Created Audit Log are automatically deleted after the number of seconds set in the environment variable from the creation time",
     "fixed_by_env_var": "This is fixed by the env var <code>{{key}}={{value}}</code>.",

+ 8 - 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,11 +1090,18 @@
     "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.",
       "need_to_fix_grant": "The permissions associated with this page must be modified in order to use the functionality correctly. <br> Please select from the options below to make the change.",
       "grant_label": {
+        "public": "Public",
         "isForbidden": "Authority not allowed to view",
         "currentPageGrantLabel": "Authorization for this page: ",
         "parentPageGrantLabel": "Authority of parent page: ",

+ 10 - 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": "エクスポート中のコレクション",
@@ -522,6 +528,7 @@
     }
   },
   "audit_log_management": {
+    "user": "ユーザー",
     "username": "ユーザー名",
     "date": "日付",
     "action": "アクション",
@@ -529,6 +536,8 @@
     "url": "URL",
     "settings": "設定",
     "return": "戻る",
+    "clear": "検索条件のクリア",
+    "reload": "再読み込み",
     "activity_expiration_date": "監査ログの有効期限",
     "activity_expiration_date_explain": "作成された監査ログは、作成時間から環境変数に設定した秒数後に自動的に削除されます",
     "fixed_by_env_var": "環境変数により固定されています <code>{{key}}={{value}}</code>.",

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

@@ -30,6 +30,7 @@
   "New": "作成",
   "Close": "閉じる",
   "Shortcuts": "ショートカット",
+  "CustomSidebar": "カスタムサイドバー",
   "eg": "例:",
   "add": "追加",
   "Undo": "元に戻す",
@@ -1082,11 +1083,18 @@
     "belonging_to_no_group": "所属しているグループが見つかりませんでした。",
     "manage_user_groups": "グループ管理"
   },
+  "crop_image_modal": {
+    "image_crop": "画像の切り抜き",
+    "crop": "トリミング",
+    "reset": "リセット",
+    "cancel": "キャンセル"
+  },
   "fix_page_grant": {
     "modal": {
       "no_grant_available": "選択可能な権限のリストが見つかりませんでした。まず親ページの権限を修正したのちに再試行してください。",
       "need_to_fix_grant": "正しく機能を使用するためにはこのページに紐づく権限を修正する必要があります。 <br> 下記の選択肢から選んで変更してください。",
       "grant_label": {
+        "public": "公開",
         "isForbidden": "権限の閲覧が許可されていません",
         "currentPageGrantLabel": "このページの権限: ",
         "parentPageGrantLabel": "親のページの権限: ",

+ 10 - 1
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。",
@@ -532,6 +538,7 @@
     }
   },
   "audit_log_management": {
+    "user": "用户",
     "username": "帐号",
     "date": "日期",
     "action": "行动",
@@ -539,6 +546,8 @@
     "url": "URL",
     "settings": "设置",
     "return": "返回",
+    "clear": "清除搜索标准",
+    "reload": "重新加载",
     "activity_expiration_date": "审计日志的到期日",
     "activity_expiration_date_explain": "创建的审计日志会在环境变量中设置的从创建时间算起的秒数后自动删除",
     "fixed_by_env_var": "这是由env var 修复的 <code>{{key}}={{value}}</code>.",

+ 8 - 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,11 +1093,18 @@
     "belonging_to_no_group": "无法找到你所属的团体。",
     "manage_user_groups": "管理用户组"
   },
+  "crop_image_modal": {
+    "image_crop": "图像裁剪",
+    "crop": "修剪",
+    "reset": "重启",
+    "cancel": "取消"
+  },
   "fix_page_grant": {
     "modal": {
       "no_grant_available": "无法找到可选择的权限列表。 请先修改父页的权限,然后再试一次。",
       "need_to_fix_grant": "为了正确使用该功能,需要修改与该页面相关的权限。 <br> 请从以下选项中选择进行更改。",
       "grant_label": {
+        "public": "向公众提供",
         "isForbidden": "无权查看的机构",
         "currentPageGrantLabel": "本页的权限: ",
         "parentPageGrantLabel": "父页的权限: ",

+ 0 - 7
packages/app/resource/cdn-manifests.js

@@ -147,13 +147,6 @@ module.exports = {
         integrity: '',
       },
     },
-    {
-      name: 'jquery-ui',
-      url: 'https://cdn.jsdelivr.net/jquery.ui/1.11.4/jquery-ui.min.css',
-      args: {
-        integrity: '',
-      },
-    },
     {
       name: 'highlight-theme-github',
       url: 'https://cdn.jsdelivr.net/npm/highlight.js@9.13.0/styles/github.css',

+ 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 - 一个知识库工具。
 公司、大学实验室和俱乐部的知识可以轻松共享,任何人都可以编辑页面。

+ 3 - 0
packages/app/src/client/interfaces/clearable.ts

@@ -0,0 +1,3 @@
+export interface IClearable {
+  clear: () => void,
+}

+ 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 - 7
packages/app/src/client/services/AdminBasicSecurityContainer.js

@@ -16,15 +16,9 @@ export default class AdminBasicSecurityContainer extends Container {
   constructor() {
     super();
 
-    this.dummyIsSameUsernameTreatedAsIdenticalUser = 0;
-    this.dummyIsSameUsernameTreatedAsIdenticalUserForError = 1;
-
     this.state = {
-      retrieveError: null,
-      // set dummy value tile for using suspense
-      isSameUsernameTreatedAsIdenticalUser: this.dummyIsSameUsernameTreatedAsIdenticalUser,
+      isSameUsernameTreatedAsIdenticalUser: false,
     };
-
   }
 
   /**

+ 0 - 54
packages/app/src/client/services/AdminCustomizeContainer.js

@@ -17,13 +17,8 @@ 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,
       isEnabledTimeline: false,
       isSavedStatesOfTabChanges: false,
       isEnabledAttachTitleHeader: false,
@@ -80,7 +75,6 @@ export default class AdminCustomizeContainer extends Container {
       const { customizeParams } = response.data;
 
       this.setState({
-        currentTheme: customizeParams.themeType,
         isEnabledTimeline: customizeParams.isEnabledTimeline,
         isSavedStatesOfTabChanges: customizeParams.isSavedStatesOfTabChanges,
         isEnabledAttachTitleHeader: customizeParams.isEnabledAttachTitleHeader,
@@ -109,17 +103,6 @@ export default class AdminCustomizeContainer extends Container {
     }
   }
 
-  /**
-   * Switch themeType
-   */
-  switchThemeType(themeName) {
-    this.setState({ currentTheme: themeName });
-
-    // preview if production
-    if (process.env.NODE_ENV !== 'development') {
-      this.previewTheme(themeName);
-    }
-  }
 
   /**
    * Switch enabledTimeLine
@@ -239,24 +222,6 @@ export default class AdminCustomizeContainer extends Container {
     this.setState({ currentCustomizeScript: inpuValue });
   }
 
-  /**
-   * Preview theme
-   * @param {string} themeName
-   */
-  async previewTheme(themeName) {
-    try {
-      // get theme asset path
-      const response = await apiv3Get('/customize-setting/theme/asset-path', { themeName });
-      const { assetPath } = response.data;
-
-      const themeLink = document.getElementById('grw-theme-link');
-      themeLink.setAttribute('href', assetPath);
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }
-
   /**
    * Preview hljs style
    * @param {string} styleId
@@ -268,25 +233,6 @@ export default class AdminCustomizeContainer extends Container {
     styleLInk.href = styleLInk.href.replace(/[^/]+\.css$/, `${styleId}.css`);
   }
 
-  /**
-   * Update theme
-   * @memberOf AdminCustomizeContainer
-   */
-  async updateCustomizeTheme() {
-    try {
-      const response = await apiv3Put('/customize-setting/theme', {
-        themeType: this.state.currentTheme,
-      });
-      const { customizedParams } = response.data;
-      this.setState({
-        themeType: customizedParams.themeType,
-      });
-    }
-    catch (err) {
-      logger.error(err);
-      throw new Error('Failed to update data');
-    }
-  }
 
   /**
    * Update function

+ 1 - 6
packages/app/src/client/services/AdminGeneralSecurityContainer.js

@@ -18,15 +18,11 @@ export default class AdminGeneralSecurityContainer extends Container {
   constructor(appContainer) {
     super();
 
-    this.dummyCurrentRestrictGuestMode = 0;
-    this.dummyCurrentRestrictGuestModeForError = 1;
-
     this.state = {
       retrieveError: null,
       sessionMaxAge: null,
       wikiMode: '',
-      // set dummy value tile for using suspense
-      currentRestrictGuestMode: this.dummyCurrentRestrictGuestMode,
+      currentRestrictGuestMode: '',
       currentPageDeletionAuthority: PageSingleDeleteConfigValue.AdminOnly,
       currentPageRecursiveDeletionAuthority: PageRecursiveDeleteConfigValue.Inherit,
       currentPageCompleteDeletionAuthority: PageSingleDeleteCompConfigValue.AdminOnly,
@@ -37,7 +33,6 @@ export default class AdminGeneralSecurityContainer extends Container {
       expandOtherOptionsForCompleteDeletion: false,
       isShowRestrictedByOwner: false,
       isShowRestrictedByGroup: false,
-      appSiteUrl: appContainer.config.crowi.url || '',
       isLocalEnabled: false,
       isLdapEnabled: false,
       isSamlEnabled: false,

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

@@ -23,7 +23,6 @@ export default class AdminGitHubSecurityContainer extends Container {
 
     this.state = {
       retrieveError: null,
-      callbackUrl: urljoin(pathUtils.removeTrailingSlash(appContainer.config.crowi.url), '/passport/github/callback'),
       // set dummy value tile for using suspense
       githubClientId: this.dummyGithubClientId,
       githubClientSecret: '',

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

@@ -23,7 +23,6 @@ export default class AdminGoogleSecurityContainer extends Container {
 
     this.state = {
       retrieveError: null,
-      callbackUrl: urljoin(pathUtils.removeTrailingSlash(appContainer.config.crowi.url), '/passport/google/callback'),
       // set dummy value tile for using suspense
       googleClientId: this.dummyGoogleClientId,
       googleClientSecret: '',

+ 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/AdminLdapSecurityContainer.js

@@ -17,13 +17,10 @@ export default class AdminLdapSecurityContainer extends Container {
     super();
 
     this.appContainer = appContainer;
-    this.dummyServerUrl = 0;
-    this.dummyServerUrlForError = 1;
 
     this.state = {
       retrieveError: null,
-      // set dummy value tile for using suspense
-      serverUrl: this.dummyServerUrl,
+      serverUrl: '',
       isUserBind: false,
       ldapBindDN: '',
       ldapBindDNPassword: '',

+ 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 - 5
packages/app/src/client/services/AdminOidcSecurityContainer.js

@@ -19,14 +19,10 @@ export default class AdminOidcSecurityContainer extends Container {
     super();
 
     this.appContainer = appContainer;
-    this.dummyOidcProviderName = 0;
-    this.dummyOidcProviderNameForError = 1;
 
     this.state = {
       retrieveError: null,
-      callbackUrl: urljoin(pathUtils.removeTrailingSlash(appContainer.config.crowi.url), '/passport/oidc/callback'),
-      // set dummy value tile for using suspense
-      oidcProviderName: this.dummyOidcProviderName,
+      oidcProviderName: '',
       oidcIssuerHost: '',
       oidcAuthorizationEndpoint: '',
       oidcTokenEndpoint: '',

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

@@ -19,17 +19,13 @@ export default class AdminSamlSecurityContainer extends Container {
     super();
 
     this.appContainer = appContainer;
-    this.dummySamlEntryPoint = 0;
-    this.dummySamlEntryPointForError = 1;
 
     this.state = {
       retrieveError: null,
       // TODO GW-1324 ABLCRure DB value takes precedence
       useOnlyEnvVars: false,
-      callbackUrl: urljoin(pathUtils.removeTrailingSlash(appContainer.config.crowi.url), '/passport/saml/callback'),
       missingMandatoryConfigKeys: [],
-      // set dummy value tile for using suspense
-      samlEntryPoint: this.dummySamlEntryPoint,
+      samlEntryPoint: '',
       samlIssuer: '',
       samlCert: '',
       samlAttrMapId: '',

+ 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 - 5
packages/app/src/client/services/AdminTwitterSecurityContainer.js

@@ -19,13 +19,9 @@ export default class AdminTwitterSecurityContainer extends Container {
     super();
 
     this.appContainer = appContainer;
-    this.dummyTwitterConsumerKey = 0;
-    this.dummyTwitterConsumerKeyForError = 1;
 
     this.state = {
-      callbackUrl: urljoin(pathUtils.removeTrailingSlash(appContainer.config.crowi.url), '/passport/twitter/callback'),
-      // set dummy value tile for using suspense
-      twitterConsumerKey: this.dummyTwitterConsumerKey,
+      twitterConsumerKey: '',
       twitterConsumerSecret: '',
       isSameUsernameTreatedAsIdenticalUser: false,
     };

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

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

@@ -13,7 +13,7 @@ const apiv3Root = '/_api/v3';
 const logger = loggerFactory('growi:apiv3');
 
 
-const apiv3ErrorHandler = (_err) => {
+const apiv3ErrorHandler = (_err: any): any[] => {
   // extract api errors from general 400 err
   const err = _err.response ? _err.response.data.errors : _err;
   const errs = toArrayIfNot(err);

+ 1 - 1
packages/app/src/client/util/i18n.js

@@ -17,7 +17,7 @@ Object.values(locales).forEach((locale) => {
 });
 
 /*
-* Note: This file will be deleted. use "~/next-i18next.config" instead
+* Note: This file will be deleted. use "^/config/next-i18next.config" instead
 */
 // extract metadata list from 'public/static/locales/${locale}/meta.json'
 export const localeMetadatas = Object.values(locales).map(locale => locale.meta);

+ 2 - 1
packages/app/src/components/Admin/App/AppSetting.jsx

@@ -3,9 +3,10 @@ import React, { useCallback } from 'react';
 import { useTranslation, i18n } from 'next-i18next';
 import PropTypes from 'prop-types';
 
+import { i18n as i18nConfig } from '^/config/next-i18next.config';
+
 import AdminAppContainer from '~/client/services/AdminAppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
-import { i18n as i18nConfig } from '~/next-i18next.config';
 import loggerFactory from '~/utils/logger';
 
 

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

+ 11 - 2
packages/app/src/components/Admin/AuditLog/ActivityTable.tsx

@@ -1,5 +1,7 @@
 import React, { FC } from 'react';
 
+import { pagePathUtils } from '@growi/core';
+import { UserPicture } from '@growi/ui';
 import { format } from 'date-fns';
 import { useTranslation } from 'react-i18next';
 
@@ -21,7 +23,7 @@ export const ActivityTable : FC<Props> = (props: Props) => {
       <table className="table table-default table-bordered table-user-list">
         <thead>
           <tr>
-            <th scope="col">{t('admin:audit_log_management.username')}</th>
+            <th scope="col">{t('admin:audit_log_management.user')}</th>
             <th scope="col">{t('admin:audit_log_management.date')}</th>
             <th scope="col">{t('admin:audit_log_management.action')}</th>
             <th scope="col">{t('admin:audit_log_management.ip')}</th>
@@ -32,7 +34,14 @@ export const ActivityTable : FC<Props> = (props: Props) => {
           {props.activityList.map((activity) => {
             return (
               <tr data-testid="activity-table" key={activity._id}>
-                <td>{activity.snapshot?.username}</td>
+                <td>
+                  { activity.user != null && (
+                    <>
+                      <UserPicture user={activity.user} className="picture rounded-circle" />
+                      <a className="ml-2" href={pagePathUtils.userPageRoot(activity.user)}>{activity.snapshot?.username}</a>
+                    </>
+                  )}
+                </td>
                 <td>{formatDate(activity.createdAt)}</td>
                 <td>{t(`admin:audit_log_action.${activity.action}`)}</td>
                 <td>{activity.ip}</td>

+ 18 - 3
packages/app/src/components/Admin/AuditLog/SearchUsernameTypeahead.tsx

@@ -1,10 +1,11 @@
 import React, {
-  FC, Fragment, useState, useCallback,
+  Fragment, useState, useCallback, useRef, ForwardRefRenderFunction, forwardRef, useImperativeHandle,
 } from 'react';
 
 import { AsyncTypeahead, Menu, MenuItem } from 'react-bootstrap-typeahead';
 import { useTranslation } from 'react-i18next';
 
+import { IClearable } from '~/client/interfaces/clearable';
 import { useSWRxUsernames } from '~/stores/user';
 
 
@@ -25,10 +26,12 @@ type Props = {
   onChange: (text: string[]) => void
 }
 
-export const SearchUsernameTypeahead: FC<Props> = (props: Props) => {
+const SearchUsernameTypeaheadSubstance: ForwardRefRenderFunction<IClearable, Props> = ((props: Props, ref) => {
   const { onChange } = props;
   const { t } = useTranslation();
 
+  const typeaheadRef = useRef<IClearable>(null);
+
   /*
    * State
    */
@@ -96,6 +99,15 @@ export const SearchUsernameTypeahead: FC<Props> = (props: Props) => {
     );
   }, []);
 
+  useImperativeHandle(ref, () => ({
+    clear() {
+      const instance = typeaheadRef?.current;
+      if (instance != null) {
+        instance.clear();
+      }
+    },
+  }));
+
   return (
     <div className="input-group mr-2">
       <div className="input-group-prepend">
@@ -104,6 +116,7 @@ export const SearchUsernameTypeahead: FC<Props> = (props: Props) => {
         </span>
       </div>
       <AsyncTypeahead
+        ref={typeaheadRef}
         id="search-username-typeahead-asynctypeahead"
         multiple
         delay={400}
@@ -119,4 +132,6 @@ export const SearchUsernameTypeahead: FC<Props> = (props: Props) => {
       />
     </div>
   );
-};
+});
+
+export const SearchUsernameTypeahead = forwardRef(SearchUsernameTypeaheadSubstance);

+ 32 - 8
packages/app/src/components/Admin/AuditLogManagement.tsx

@@ -1,8 +1,11 @@
-import React, { FC, useState, useCallback } from 'react';
+import React, {
+  FC, useState, useCallback, useRef,
+} from 'react';
 
 import { format } from 'date-fns';
 import { useTranslation } from 'react-i18next';
 
+import { IClearable } from '~/client/interfaces/clearable';
 import { toastError } from '~/client/util/apiNotification';
 import { SupportedActionType } from '~/interfaces/activity';
 import { useSWRxActivity } from '~/stores/activity';
@@ -17,7 +20,6 @@ import { DateRangePicker } from './AuditLog/DateRangePicker';
 import { SearchUsernameTypeahead } from './AuditLog/SearchUsernameTypeahead';
 import { SelectActionDropdown } from './AuditLog/SelectActionDropdown';
 
-
 const formatDate = (date: Date | null) => {
   if (date == null) {
     return '';
@@ -30,8 +32,9 @@ const PAGING_LIMIT = 10;
 export const AuditLogManagement: FC = () => {
   const { t } = useTranslation();
 
+  const typeaheadRef = useRef<IClearable>(null);
+
   const { data: auditLogAvailableActionsData } = useAuditLogAvailableActions();
-  const auditLogAvailableActions = auditLogAvailableActionsData != null ? auditLogAvailableActionsData : [];
 
   /*
    * State
@@ -43,7 +46,7 @@ export const AuditLogManagement: FC = () => {
   const [endDate, setEndDate] = useState<Date | null>(null);
   const [selectedUsernames, setSelectedUsernames] = useState<string[]>([]);
   const [actionMap, setActionMap] = useState(
-    new Map<SupportedActionType, boolean>(auditLogAvailableActions.map(action => [action, true])),
+    new Map<SupportedActionType, boolean>(auditLogAvailableActionsData != null ? auditLogAvailableActionsData.map(action => [action, true]) : []),
   );
 
   /*
@@ -94,6 +97,18 @@ export const AuditLogManagement: FC = () => {
     setSelectedUsernames(usernames);
   }, []);
 
+  const clearButtonPushedHandler = useCallback(() => {
+    setActivePage(1);
+    setStartDate(null);
+    setEndDate(null);
+    setSelectedUsernames([]);
+    typeaheadRef.current?.clear();
+
+    if (auditLogAvailableActionsData != null) {
+      setActionMap(new Map<SupportedActionType, boolean>(auditLogAvailableActionsData.map(action => [action, true])));
+    }
+  }, [setActivePage, setStartDate, setEndDate, setSelectedUsernames, setActionMap, auditLogAvailableActionsData]);
+
   const reloadButtonPushedHandler = useCallback(() => {
     setActivePage(1);
     mutateActivity();
@@ -128,6 +143,7 @@ export const AuditLogManagement: FC = () => {
         <>
           <div className="form-inline mb-3">
             <SearchUsernameTypeahead
+              ref={typeaheadRef}
               onChange={setUsernamesHandler}
             />
 
@@ -139,14 +155,22 @@ export const AuditLogManagement: FC = () => {
 
             <SelectActionDropdown
               actionMap={actionMap}
-              availableActions={auditLogAvailableActions}
+              availableActions={auditLogAvailableActionsData || []}
               onChangeAction={actionCheckboxChangedHandler}
               onChangeMultipleAction={multipleActionCheckboxChangedHandler}
             />
 
-            <button type="button" className="btn ml-auto grw-btn-reload" onClick={reloadButtonPushedHandler}>
-              <i className="icon icon-reload" />
-            </button>
+            <div className="ml-auto">
+              <button type="button" className="btn btn-outline-secondary btn-sm mr-2" onClick={clearButtonPushedHandler}>
+                <span className="icon-refresh mr-1" />
+                {t('admin:audit_log_management.clear')}
+              </button>
+
+              <button type="button" className="btn btn-outline-secondary btn-sm" onClick={reloadButtonPushedHandler}>
+                <i className="icon icon-reload mr-1" />
+                {t('admin:audit_log_management.reload')}
+              </button>
+            </div>
           </div>
 
           <p

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

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

@@ -1,15 +1,13 @@
 
-import React, { Fragment } from 'react';
+import React, { useEffect, useCallback } 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,33 +23,29 @@ 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 });
-      }
-    })();
-  }
+  const fetchCustomizeSettingsData = useCallback(async() => {
+    try {
+      await adminCustomizeContainer.retrieveCustomizeData();
+    }
+    catch (err) {
+      const errs = toArrayIfNot(err);
+      toastError(errs);
+      logger.error(errs);
+    }
+  }, [adminCustomizeContainer]);
+
+  useEffect(() => {
+    fetchCustomizeSettingsData();
+  }, [fetchCustomizeSettingsData]);
 
-  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 />
@@ -76,14 +71,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>

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

@@ -12,7 +12,7 @@ const CustomizeLayoutSetting = (): JSX.Element => {
   const { resolvedTheme } = useNextThemes();
 
   const [isContainerFluid, setIsContainerFluid] = useState(false);
-  const [retrieveError, setRetrieveError] = useState();
+  const [retrieveError, setRetrieveError] = useState<any>();
 
   const retrieveData = useCallback(async() => {
     try {

+ 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<any>();
+  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" />

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

@@ -78,7 +78,7 @@ const CustomizeSidebarsetting = (): JSX.Element => {
                 id="is-open"
                 className="custom-control-input"
                 name="mailVisibility"
-                checked={!isSidebarDrawerMode && !isSidebarClosedAtDockMode}
+                checked={isSidebarDrawerMode === false && isSidebarClosedAtDockMode === false}
                 disabled={isSidebarDrawerMode}
                 onChange={() => setIsSidebarClosedAtDockMode(false)}
               />
@@ -92,7 +92,7 @@ const CustomizeSidebarsetting = (): JSX.Element => {
                 id="is-closed"
                 className="custom-control-input"
                 name="mailVisibility"
-                checked={!isSidebarDrawerMode && isSidebarClosedAtDockMode}
+                checked={isSidebarDrawerMode === false && isSidebarClosedAtDockMode === true}
                 disabled={isSidebarDrawerMode}
                 onChange={() => setIsSidebarClosedAtDockMode(true)}
               />

+ 8 - 4
packages/app/src/components/Admin/Customize/CustomizeThemeOptions.jsx

@@ -49,9 +49,11 @@ const uniqueTheme = [{
 
 const CustomizeThemeOptions = (props) => {
 
-  const { adminCustomizeContainer } = props;
+  const { adminCustomizeContainer, currentTheme } = props;
+  const { currentLayout } = adminCustomizeContainer.state;
+
   const { t } = useTranslation();
-  const { currentLayout, currentTheme } = adminCustomizeContainer.state;
+
 
   return (
     <div id="themeOptions" className={`${currentLayout === 'kibela' && 'disabled'}`}>
@@ -64,7 +66,7 @@ const CustomizeThemeOptions = (props) => {
               <ThemeColorBox
                 key={theme.name}
                 isSelected={currentTheme === theme.name}
-                onSelected={() => adminCustomizeContainer.switchThemeType(theme.name)}
+                onSelected={() => props.onSelected(theme.name)}
                 {...theme}
               />
             );
@@ -80,7 +82,7 @@ const CustomizeThemeOptions = (props) => {
               <ThemeColorBox
                 key={theme.name}
                 isSelected={currentTheme === theme.name}
-                onSelected={() => adminCustomizeContainer.switchThemeType(theme.name)}
+                onSelected={() => props.onSelected(theme.name)}
                 {...theme}
               />
             );
@@ -96,6 +98,8 @@ const CustomizeThemeOptionsWrapper = withUnstatedContainers(CustomizeThemeOption
 
 CustomizeThemeOptions.propTypes = {
   adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
+  onSelected: PropTypes.func,
+  currentTheme: PropTypes.string,
 };
 
 export default CustomizeThemeOptionsWrapper;

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

@@ -4,6 +4,9 @@ import { useTranslation } from 'next-i18next';
 
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { apiv3Put } from '~/client/util/apiv3-client';
+import { useGrowiTheme } from '~/stores/context';
+
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
@@ -17,39 +20,36 @@ type Props = {
 const CustomizeThemeSetting = (props: Props): JSX.Element => {
 
   const { adminCustomizeContainer } = props;
+  const { data: currentTheme, mutate: mutateGrowiTheme } = useGrowiTheme();
   const { t } = useTranslation();
 
-  const onClickSubmit = useCallback(async() => {
+  const selectedHandler = useCallback((themeName) => {
+    mutateGrowiTheme(themeName);
+  }, [mutateGrowiTheme]);
+
+  const submitHandler = useCallback(async() => {
     try {
-      await adminCustomizeContainer.updateCustomizeTheme();
+      if (currentTheme != null) {
+        await apiv3Put('/customize-setting/theme', {
+          themeType: currentTheme,
+        });
+      }
+
       toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.theme') }));
     }
     catch (err) {
       toastError(err);
     }
-  }, [t, adminCustomizeContainer]);
-
-  const renderDevAlert = useCallback(() => {
-    if (process.env.NODE_ENV === 'development') {
-      return (
-        <div className="alert alert-warning">
-          <strong>DEBUG MESSAGE:</strong> Live preview for theme is disabled in development mode.
-        </div>
-      );
-    }
-  }, []);
+  }, [currentTheme, t]);
 
   return (
-    <React.Fragment>
-      <div className="row">
-        <div className="col-12">
-          <h2 className="admin-setting-header">{t('admin:customize_setting.theme')}</h2>
-          {renderDevAlert()}
-          <CustomizeThemeOptions />
-          <AdminUpdateButtonRow onClick={onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
-        </div>
+    <div className="row">
+      <div className="col-12">
+        <h2 className="admin-setting-header">{t('admin:customize_setting.theme')}</h2>
+        <CustomizeThemeOptions onSelected={selectedHandler} currentTheme={currentTheme} />
+        <AdminUpdateButtonRow onClick={submitHandler} disabled={adminCustomizeContainer.state.retrieveError != null} />
       </div>
-    </React.Fragment>
+    </div>
   );
 };
 

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

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

@@ -0,0 +1,214 @@
+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: unknown) {
+      setIsConnected(false);
+
+      // evaluate whether configured or not
+      if (Array.isArray(errors)) {
+        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"

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

@@ -1,50 +1,41 @@
-/* eslint-disable react/no-danger */
-import React from 'react';
+import React, { useEffect, useCallback } 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';
 
-let retrieveErrors = null;
-function BasicSecurityManagement(props) {
+const BasicSecurityManagement = (props) => {
   const { adminBasicSecurityContainer } = props;
-  if (adminBasicSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser === adminBasicSecurityContainer.dummyIsSameUsernameTreatedAsIdenticalUser) {
-    throw (async() => {
-      try {
-        await adminBasicSecurityContainer.retrieveSecurityData();
-      }
-      catch (err) {
-        const errs = toArrayIfNot(err);
-        toastError(errs);
-        retrieveErrors = errs;
-        adminBasicSecurityContainer.setState({
-          isSameUsernameTreatedAsIdenticalUser: adminBasicSecurityContainer.dummyIsSameUsernameTreatedAsIdenticalUser,
-        });
-
-      }
-    })();
-  }
-
-  if (
-    adminBasicSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser === adminBasicSecurityContainer.dummyIsSameUsernameTreatedAsIdenticalUserForError
-  ) {
-    throw new Error(`${retrieveErrors.length} errors occured`);
-  }
+
+  const fetchBasicSecuritySettingsData = useCallback(async() => {
+    try {
+      await adminBasicSecurityContainer.retrieveSecurityData();
+    }
+    catch (err) {
+      const errs = toArrayIfNot(err);
+      toastError(errs);
+    }
+  }, [adminBasicSecurityContainer]);
+
+  useEffect(() => {
+    fetchBasicSecuritySettingsData();
+  }, [adminBasicSecurityContainer, fetchBasicSecuritySettingsData]);
+
 
   return <BasicSecurityManagementContents />;
-}
+};
 
 BasicSecurityManagement.propTypes = {
   adminBasicSecurityContainer: PropTypes.instanceOf(AdminBasicSecurityContainer).isRequired,
 };
 
-const BasicSecurityManagementWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(BasicSecurityManagement), [
+const BasicSecurityManagementWithUnstatedContainer = withUnstatedContainers(BasicSecurityManagement, [
   AdminBasicSecurityContainer,
 ]);
 

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

@@ -1,4 +1,3 @@
-/* eslint-disable react/no-danger */
 import React from 'react';
 
 import { withTranslation } from 'next-i18next';
@@ -14,15 +13,13 @@ class FacebookSecurityManagement extends React.Component {
   render() {
     const { t } = this.props;
     return (
-      <React.Fragment>
-
+      <>
         <h2 className="alert-anchor border-bottom">
           Facebook OAuth { t('security_setting.configuration') }
         </h2>
 
         <p className="well">(TBD)</p>
-
-      </React.Fragment>
+      </>
     );
   }
 

+ 22 - 26
packages/app/src/components/Admin/Security/GitHubSecuritySetting.jsx

@@ -1,46 +1,42 @@
-/* eslint-disable react/no-danger */
-import React from 'react';
+import React, { useEffect, useCallback } 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';
 
-let retrieveErrors = null;
-function GitHubSecurityManagement(props) {
+const GitHubSecurityManagement = (props) => {
   const { adminGitHubSecurityContainer } = props;
-  if (adminGitHubSecurityContainer.state.githubClientId === adminGitHubSecurityContainer.dummyGithubClientId) {
-    throw (async() => {
-      try {
-        await adminGitHubSecurityContainer.retrieveSecurityData();
-      }
-      catch (err) {
-        const errs = toArrayIfNot(err);
-        toastError(errs);
-        retrieveErrors = errs;
-        adminGitHubSecurityContainer.setState({ githubClientId: adminGitHubSecurityContainer.dummyGithubClientIdForError });
-      }
-    })();
-  }
-
-  if (adminGitHubSecurityContainer.state.githubClientId === adminGitHubSecurityContainer.dummyGithubClientIdForError) {
-    throw new Error(`${retrieveErrors.length} errors occured`);
-  }
+
+  const fetchGitHubSecuritySettingsData = useCallback(async() => {
+    try {
+      await adminGitHubSecurityContainer.retrieveSecurityData();
+    }
+    catch (err) {
+      const errs = toArrayIfNot(err);
+      toastError(errs);
+    }
+  }, [adminGitHubSecurityContainer]);
+
+  useEffect(() => {
+    fetchGitHubSecuritySettingsData();
+  }, [adminGitHubSecurityContainer, fetchGitHubSecuritySettingsData]);
 
   return <GitHubSecuritySettingContents />;
-}
+};
 
 
 GitHubSecurityManagement.propTypes = {
   adminGitHubSecurityContainer: PropTypes.instanceOf(AdminGitHubSecurityContainer).isRequired,
 };
 
-const GitHubSecurityManagementWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(GitHubSecurityManagement), [
+const GitHubSecurityManagementWithUnstatedContainer = withUnstatedContainers(GitHubSecurityManagement, [
   AdminGitHubSecurityContainer,
 ]);
 

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

@@ -1,13 +1,16 @@
 /* eslint-disable react/no-danger */
 import React from 'react';
 
-import PropTypes from 'prop-types';
+import { pathUtils } from '@growi/core';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
+import urljoin from 'url-join';
 
 
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminGitHubSecurityContainer from '~/client/services/AdminGitHubSecurityContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { useSiteUrl } from '~/stores/context';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
@@ -33,8 +36,11 @@ class GitHubSecurityManagementContents extends React.Component {
   }
 
   render() {
-    const { t, adminGeneralSecurityContainer, adminGitHubSecurityContainer } = this.props;
+    const {
+      t, adminGeneralSecurityContainer, adminGitHubSecurityContainer, siteUrl,
+    } = this.props;
     const { isGitHubEnabled } = adminGeneralSecurityContainer.state;
+    const gitHubCallbackUrl = urljoin(pathUtils.removeTrailingSlash(siteUrl), '/passport/github/callback');
 
     return (
 
@@ -75,11 +81,11 @@ class GitHubSecurityManagementContents extends React.Component {
             <input
               className="form-control"
               type="text"
-              value={adminGitHubSecurityContainer.state.appSiteUrl}
+              value={gitHubCallbackUrl}
               readOnly
             />
             <p className="form-text text-muted small">{t('security_setting.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
-            {!adminGeneralSecurityContainer.state.appSiteUrl && (
+            {(siteUrl == null || siteUrl === '') && (
               <div className="alert alert-danger">
                 <i
                   className="icon-exclamation"
@@ -172,7 +178,7 @@ class GitHubSecurityManagementContents extends React.Component {
           <ol id="collapseHelpForGitHubOauth" className="collapse">
             {/* eslint-disable-next-line max-len */}
             <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.GitHub.register_1', { link: '<a href="https://github.com/settings/developers" target=_blank>GitHub Developer Settings</a>' }) }} />
-            <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.GitHub.register_2', { url: adminGitHubSecurityContainer.state.callbackUrl }) }} />
+            <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.GitHub.register_2', { url: gitHubCallbackUrl }) }} />
             <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.GitHub.register_3') }} />
           </ol>
         </div>
@@ -187,7 +193,8 @@ class GitHubSecurityManagementContents extends React.Component {
 
 const GitHubSecurityManagementContentsFC = (props) => {
   const { t } = useTranslation();
-  return <GitHubSecurityManagementContents t={t} {...props} />;
+  const { data: siteUrl } = useSiteUrl();
+  return <GitHubSecurityManagementContents t={t} siteUrl={siteUrl} {...props} />;
 };
 
 /**

+ 23 - 26
packages/app/src/components/Admin/Security/GoogleSecuritySetting.jsx

@@ -1,45 +1,42 @@
-/* eslint-disable react/no-danger */
-import React from 'react';
+import React, { useEffect, useCallback } 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;
-function GoogleSecurityManagement(props) {
+const GoogleSecurityManagement = (props) => {
   const { adminGoogleSecurityContainer } = props;
-  if (adminGoogleSecurityContainer.state.googleClientId === adminGoogleSecurityContainer.dummyGoogleClientId) {
-    throw (async() => {
-      try {
-        await adminGoogleSecurityContainer.retrieveSecurityData();
-      }
-      catch (err) {
-        const errs = toArrayIfNot(err);
-        toastError(errs);
-        retrieveErrors = errs;
-        adminGoogleSecurityContainer.setState({ googleClientId: adminGoogleSecurityContainer.dummyGoogleClientIdForError });
-      }
-    })();
-  }
-
-  if (adminGoogleSecurityContainer.state.googleClientId === adminGoogleSecurityContainer.dummyGoogleClientIdForError) {
-    throw new Error(`${retrieveErrors.length} errors occured`);
-  }
+
+  const fetchGoogleSecuritySettingsData = useCallback(async() => {
+    try {
+      await adminGoogleSecurityContainer.retrieveSecurityData();
+    }
+    catch (err) {
+      const errs = toArrayIfNot(err);
+      toastError(errs);
+    }
+  }, [adminGoogleSecurityContainer]);
+
+
+  useEffect(() => {
+    fetchGoogleSecuritySettingsData();
+  }, [adminGoogleSecurityContainer, fetchGoogleSecuritySettingsData]);
 
   return <GoogleSecurityManagementContents />;
-}
+};
 
 
 GoogleSecurityManagement.propTypes = {
   adminGoogleSecurityContainer: PropTypes.instanceOf(AdminGoogleSecurityContainer).isRequired,
 };
 
-const GoogleSecurityManagementWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(GoogleSecurityManagement), [
+const GoogleSecurityManagementWithUnstatedContainer = withUnstatedContainers(GoogleSecurityManagement, [
   AdminGoogleSecurityContainer,
 ]);
 

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

@@ -1,13 +1,14 @@
-/* eslint-disable react/no-danger */
 import React from 'react';
 
-import PropTypes from 'prop-types';
+import { pathUtils } from '@growi/core';
 import { useTranslation } from 'next-i18next';
-
+import PropTypes from 'prop-types';
+import urljoin from 'url-join';
 
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminGoogleSecurityContainer from '~/client/services/AdminGoogleSecurityContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { useSiteUrl } from '~/stores/context';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
@@ -33,8 +34,11 @@ class GoogleSecurityManagementContents extends React.Component {
   }
 
   render() {
-    const { t, adminGeneralSecurityContainer, adminGoogleSecurityContainer } = this.props;
+    const {
+      t, adminGeneralSecurityContainer, adminGoogleSecurityContainer, siteUrl,
+    } = this.props;
     const { isGoogleEnabled } = adminGeneralSecurityContainer.state;
+    const googleCallbackUrl = urljoin(pathUtils.removeTrailingSlash(siteUrl), '/passport/google/callback');
 
     return (
 
@@ -75,11 +79,11 @@ class GoogleSecurityManagementContents extends React.Component {
             <input
               className="form-control"
               type="text"
-              value={adminGoogleSecurityContainer.state.callbackUrl}
+              value={googleCallbackUrl}
               readOnly
             />
             <p className="form-text text-muted small">{t('security_setting.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
-            {!adminGeneralSecurityContainer.state.appSiteUrl && (
+            {(siteUrl == null || siteUrl === '') && (
               <div className="alert alert-danger">
                 <i
                   className="icon-exclamation"
@@ -179,7 +183,7 @@ class GoogleSecurityManagementContents extends React.Component {
             <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.Google.register_1', { link: '<a href="https://console.cloud.google.com/apis/credentials" target=_blank>Google Cloud Platform API Manager</a>' }) }} />
             <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.Google.register_2') }} />
             <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.Google.register_3') }} />
-            <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.Google.register_4', { url: adminGoogleSecurityContainer.state.callbackUrl }) }} />
+            <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.Google.register_4', { url: googleCallbackUrl }) }} />
             <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.Google.register_5') }} />
           </ol>
         </div>
@@ -194,7 +198,8 @@ class GoogleSecurityManagementContents extends React.Component {
 
 const GoogleSecurityManagementContentsFc = (props) => {
   const { t } = useTranslation();
-  return <GoogleSecurityManagementContents t={t} {...props} />;
+  const { data: siteUrl } = useSiteUrl();
+  return <GoogleSecurityManagementContents t={t} siteUrl={siteUrl} {...props} />;
 };
 
 
@@ -202,6 +207,7 @@ GoogleSecurityManagementContents.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
   adminGoogleSecurityContainer: PropTypes.instanceOf(AdminGoogleSecurityContainer).isRequired,
+  siteUrl: PropTypes.string,
 };
 
 const GoogleSecurityManagementContentsWrapper = withUnstatedContainers(GoogleSecurityManagementContentsFc, [

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

@@ -1,44 +1,40 @@
-import React from 'react';
+import React, { useEffect, useCallback } 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';
 
-let retrieveErrors = null;
-function LdapSecuritySetting(props) {
+const LdapSecuritySetting = (props) => {
   const { adminLdapSecurityContainer } = props;
-  if (adminLdapSecurityContainer.state.serverUrl === adminLdapSecurityContainer.dummyServerUrl) {
-    throw (async() => {
-      try {
-        await adminLdapSecurityContainer.retrieveSecurityData();
-      }
-      catch (err) {
-        const errs = toArrayIfNot(err);
-        toastError(errs);
-        retrieveErrors = errs;
-        adminLdapSecurityContainer.setState({ serverUrl: adminLdapSecurityContainer.dummyServerUrlForError });
-      }
-    })();
-  }
-
-  if (adminLdapSecurityContainer.state.serverUrl === adminLdapSecurityContainer.dummyServerUrlForError) {
-    throw new Error(`${retrieveErrors.length} errors occured`);
-  }
+
+  const fetchLdapSecuritySettingsData = useCallback(async() => {
+    try {
+      await adminLdapSecurityContainer.retrieveSecurityData();
+    }
+    catch (err) {
+      const errs = toArrayIfNot(err);
+      toastError(errs);
+    }
+  }, [adminLdapSecurityContainer]);
+
+  useEffect(() => {
+    fetchLdapSecuritySettingsData();
+  }, [adminLdapSecurityContainer, fetchLdapSecuritySettingsData]);
 
   return <LdapSecuritySettingContents />;
-}
+};
 
 LdapSecuritySetting.propTypes = {
   adminLdapSecurityContainer: PropTypes.instanceOf(AdminLdapSecurityContainer).isRequired,
 };
 
-const LdapSecuritySettingWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(LdapSecuritySetting), [
+const LdapSecuritySettingWithUnstatedContainer = withUnstatedContainers(LdapSecuritySetting, [
   AdminLdapSecurityContainer,
 ]);
 

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

@@ -1,45 +1,41 @@
-/* eslint-disable react/no-danger */
-import React from 'react';
+import React, { useEffect, useCallback } 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';
 
-let retrieveErrors = null;
-function LocalSecuritySetting(props) {
+const LocalSecuritySetting = (props) => {
   const { adminLocalSecurityContainer } = props;
-  if (adminLocalSecurityContainer.state.registrationMode === adminLocalSecurityContainer.dummyRegistrationMode) {
-    throw (async() => {
-      try {
-        await adminLocalSecurityContainer.retrieveSecurityData();
-      }
-      catch (err) {
-        const errs = toArrayIfNot(err);
-        toastError(errs);
-        retrieveErrors = errs;
-        adminLocalSecurityContainer.setState({ registrationMode: adminLocalSecurityContainer.dummyRegistrationModeForError });
-      }
-    })();
-  }
-
-  if (adminLocalSecurityContainer.state.registrationMode === adminLocalSecurityContainer.dummyRegistrationModeForError) {
-    throw new Error(`${retrieveErrors.length} errors occured`);
-  }
+
+  const fetchLocalSecuritySettingsData = useCallback(async() => {
+    try {
+      await adminLocalSecurityContainer.retrieveSecurityData();
+    }
+    catch (err) {
+      const errs = toArrayIfNot(err);
+      toastError(errs);
+    }
+  }, [adminLocalSecurityContainer]);
+
+
+  useEffect(() => {
+    fetchLocalSecuritySettingsData();
+  }, [adminLocalSecurityContainer, fetchLocalSecuritySettingsData]);
 
   return <LocalSecuritySettingContents />;
-}
+};
 
 LocalSecuritySetting.propTypes = {
   adminLocalSecurityContainer: PropTypes.instanceOf(AdminLocalSecurityContainer).isRequired,
 };
 
-const LocalSecuritySettingWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(LocalSecuritySetting), [
+const LocalSecuritySettingWithUnstatedContainer = withUnstatedContainers(LocalSecuritySetting, [
   AdminLocalSecurityContainer,
 ]);
 

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

@@ -1,14 +1,13 @@
-/* eslint-disable react/no-danger */
 import React from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminLocalSecurityContainer from '~/client/services/AdminLocalSecurityContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { useIsMailerSetup } from '~/stores/context';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
@@ -37,14 +36,13 @@ class LocalSecuritySettingContents extends React.Component {
       t,
       adminGeneralSecurityContainer,
       adminLocalSecurityContainer,
-      appContainer,
+      isMailerSetup,
     } = this.props;
     const { registrationMode, isPasswordResetEnabled, isEmailAuthenticationEnabled } = adminLocalSecurityContainer.state;
     const { isLocalEnabled } = adminGeneralSecurityContainer.state;
-    const { isMailerSetup } = appContainer.config;
 
     return (
-      <React.Fragment>
+      <>
         {adminLocalSecurityContainer.state.retrieveError != null && (
           <div className="alert alert-danger">
             <p>
@@ -97,7 +95,7 @@ class LocalSecuritySettingContents extends React.Component {
         </div>
 
         {isLocalEnabled && (
-          <React.Fragment>
+          <>
             <h3 className="border-bottom">{t('security_setting.configuration')}</h3>
 
             <div className="row">
@@ -236,9 +234,9 @@ class LocalSecuritySettingContents extends React.Component {
                 </button>
               </div>
             </div>
-          </React.Fragment>
+          </>
         )}
-      </React.Fragment>
+      </>
     );
   }
 
@@ -246,18 +244,17 @@ class LocalSecuritySettingContents extends React.Component {
 
 LocalSecuritySettingContents.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
   adminLocalSecurityContainer: PropTypes.instanceOf(AdminLocalSecurityContainer).isRequired,
 };
 
 const LocalSecuritySettingContentsWrapperFC = (props) => {
   const { t } = useTranslation();
-  return <LocalSecuritySettingContents t={t} {...props} />;
+  const { data: isMailerSetup } = useIsMailerSetup();
+  return <LocalSecuritySettingContents t={t} {...props} isMailerSetup={isMailerSetup ?? false} />;
 };
 
 const LocalSecuritySettingContentsWrapper = withUnstatedContainers(LocalSecuritySettingContentsWrapperFC, [
-  AppContainer,
   AdminGeneralSecurityContainer,
   AdminLocalSecurityContainer,
 ]);

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