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

Merge branch 'support/apply-nextjs-2' of https://github.com/weseek/growi into support/apply-nextjs-loginPage

Shunm634-source 3 лет назад
Родитель
Сommit
b244ae5382
100 измененных файлов с 1752 добавлено и 1125 удалено
  1. 0 1
      .github/workflows/ci-app.yml
  2. 10 1
      .github/workflows/reusable-app-prod.yml
  3. 1 1
      lerna.json
  4. 2 2
      package.json
  5. 5 1
      packages/app/.env.development
  6. 2 2
      packages/app/bin/github-actions/update-readme.sh
  7. 83 0
      packages/app/config/rate-limiter.ts
  8. 0 3
      packages/app/docker/Dockerfile
  9. 6 6
      packages/app/docker/README.md
  10. 24 4
      packages/app/next.config.js
  11. 23 21
      packages/app/package.json
  12. 183 0
      packages/app/public/static/locales/en_US/admin/admin.json
  13. 8 0
      packages/app/public/static/locales/en_US/translation.json
  14. 0 2
      packages/app/public/static/locales/index.js
  15. 183 0
      packages/app/public/static/locales/ja_JP/admin/admin.json
  16. 8 0
      packages/app/public/static/locales/ja_JP/translation.json
  17. 183 0
      packages/app/public/static/locales/zh_CN/admin/admin.json
  18. 8 0
      packages/app/public/static/locales/zh_CN/translation.json
  19. 1 1
      packages/app/resource/locales/en_US/welcome.md
  20. 1 1
      packages/app/resource/locales/ja_JP/welcome.md
  21. 1 1
      packages/app/resource/locales/zh_CN/welcome.md
  22. 2 0
      packages/app/src/client/admin.jsx
  23. 4 16
      packages/app/src/client/app.jsx
  24. 2 10
      packages/app/src/client/base.jsx
  25. 0 4
      packages/app/src/client/boot.js
  26. 0 4
      packages/app/src/client/legacy/crowi.js
  27. 4 4
      packages/app/src/client/models/MarkdownTable.js
  28. 1 2
      packages/app/src/client/plugin.js
  29. 1 24
      packages/app/src/client/services/AdminSocketIoContainer.js
  30. 12 35
      packages/app/src/client/services/AppContainer.js
  31. 42 21
      packages/app/src/client/services/ContextExtractor.tsx
  32. 2 5
      packages/app/src/client/services/PageContainer.js
  33. 0 186
      packages/app/src/client/services/PersonalContainer.js
  34. 1 2
      packages/app/src/client/services/page-operation.ts
  35. 0 208
      packages/app/src/client/util/GrowiRenderer.js
  36. 0 23
      packages/app/src/client/util/PreProcessor/XssFilter.js
  37. 1 0
      packages/app/src/client/util/blink-section-header.ts
  38. 0 73
      packages/app/src/client/util/color-scheme.js
  39. 4 0
      packages/app/src/client/util/i18n.js
  40. 0 17
      packages/app/src/client/util/markdown-it/blockdiag.js
  41. 0 9
      packages/app/src/client/util/markdown-it/drawio-viewer.js
  42. 0 12
      packages/app/src/client/util/markdown-it/emoji.js
  43. 0 16
      packages/app/src/client/util/markdown-it/mathjax.js
  44. 0 18
      packages/app/src/client/util/markdown-it/task-lists.js
  45. 0 27
      packages/app/src/client/util/markdown-it/toc-and-anchor.js
  46. 4 7
      packages/app/src/client/util/reveal/plugins/growi-renderer.js
  47. 11 9
      packages/app/src/components/Admin/AdminHome/AdminHome.jsx
  48. 23 18
      packages/app/src/components/Admin/App/AppSetting.jsx
  49. 47 0
      packages/app/src/components/Admin/AuditLog/ActivityTable.tsx
  50. 26 0
      packages/app/src/components/Admin/AuditLog/AuditLogDisableMode.tsx
  51. 54 0
      packages/app/src/components/Admin/AuditLog/AuditLogSettings.tsx
  52. 67 0
      packages/app/src/components/Admin/AuditLog/DateRangePicker.tsx
  53. 122 0
      packages/app/src/components/Admin/AuditLog/SearchUsernameTypeahead.tsx
  54. 123 0
      packages/app/src/components/Admin/AuditLog/SelectActionDropdown.tsx
  55. 181 0
      packages/app/src/components/Admin/AuditLogManagement.tsx
  56. 16 11
      packages/app/src/components/Admin/Common/AdminNavigation.jsx
  57. 5 6
      packages/app/src/components/Admin/Customize/CustomizeLayoutSetting.tsx
  58. 4 5
      packages/app/src/components/Admin/Customize/CustomizeSidebarSetting.tsx
  59. 17 16
      packages/app/src/components/Admin/Customize/CustomizeThemeOptions.jsx
  60. 1 1
      packages/app/src/components/Admin/ElasticsearchManagement/StatusTable.jsx
  61. 45 51
      packages/app/src/components/Admin/ExportArchiveDataPage.jsx
  62. 5 4
      packages/app/src/components/Admin/ImportData/GrowiArchiveSection.jsx
  63. 1 1
      packages/app/src/components/Admin/ImportData/ImportDataPageContents.jsx
  64. 0 1
      packages/app/src/components/Admin/ManageExternalAccount.jsx
  65. 2 4
      packages/app/src/components/Admin/MarkdownSetting/PresentationForm.jsx
  66. 2 4
      packages/app/src/components/Admin/MarkdownSetting/WhiteListInput.jsx
  67. 2 4
      packages/app/src/components/Admin/MarkdownSetting/XssForm.jsx
  68. 2 4
      packages/app/src/components/Admin/Notification/GlobalNotification.jsx
  69. 2 4
      packages/app/src/components/Admin/Notification/GlobalNotificationList.jsx
  70. 9 12
      packages/app/src/components/Admin/Notification/NotificationSetting.jsx
  71. 2 4
      packages/app/src/components/Admin/Notification/UserNotificationRow.jsx
  72. 2 4
      packages/app/src/components/Admin/Notification/UserTriggerNotification.jsx
  73. 5 5
      packages/app/src/components/Admin/Security/FacebookSecuritySetting.jsx
  74. 2 0
      packages/app/src/components/Admin/Security/LdapAuthTest.jsx
  75. 21 21
      packages/app/src/components/Admin/Security/SecurityManagementContents.jsx
  76. 4 6
      packages/app/src/components/Admin/Security/ShareLinkSetting.jsx
  77. 2 4
      packages/app/src/components/Admin/SlackIntegration/WithProxyAccordions.jsx
  78. 23 25
      packages/app/src/components/Admin/UserGroup/UserGroupDeleteModal.tsx
  79. 2 4
      packages/app/src/components/Admin/UserGroup/UserGroupForm.tsx
  80. 4 6
      packages/app/src/components/Admin/UserGroup/UserGroupModal.tsx
  81. 8 9
      packages/app/src/components/Admin/UserGroup/UserGroupPage.tsx
  82. 9 12
      packages/app/src/components/Admin/UserGroup/UserGroupTable.tsx
  83. 2 1
      packages/app/src/components/Admin/UserGroupDetail/UserGroupUserFormByInput.jsx
  84. 2 1
      packages/app/src/components/Admin/UserGroupDetail/UserGroupUserTable.jsx
  85. 4 3
      packages/app/src/components/Admin/UserManagement.jsx
  86. 2 4
      packages/app/src/components/Admin/Users/GiveAdminButton.jsx
  87. 2 4
      packages/app/src/components/Admin/Users/InviteUserControl.jsx
  88. 3 6
      packages/app/src/components/Admin/Users/PasswordResetModal.jsx
  89. 3 5
      packages/app/src/components/Admin/Users/SendInvitationEmailButton.jsx
  90. 2 4
      packages/app/src/components/Admin/Users/StatusActivateButton.jsx
  91. 1 1
      packages/app/src/components/Admin/Users/UserInviteModal.jsx
  92. 2 4
      packages/app/src/components/Admin/Users/UserMenu.jsx
  93. 2 4
      packages/app/src/components/Admin/Users/UserRemoveButton.jsx
  94. 0 49
      packages/app/src/components/BasicLayout.tsx
  95. 18 0
      packages/app/src/components/BookmarkButtons.module.scss
  96. 9 2
      packages/app/src/components/BookmarkButtons.tsx
  97. 2 1
      packages/app/src/components/CompleteUserRegistrationForm.tsx
  98. 4 4
      packages/app/src/components/DescendantsPageList.tsx
  99. 1 0
      packages/app/src/components/DescendantsPageListModal.tsx
  100. 30 7
      packages/app/src/components/Drawio.tsx

+ 0 - 1
.github/workflows/ci-app.yml

@@ -7,7 +7,6 @@ on:
       - rc/**
       - chore/**
       - support/prepare-v**
-      - support/apply-nextjs-2
     paths:
       - .github/workflows/ci-app.yml
       - .eslint*

+ 10 - 1
.github/workflows/reusable-app-prod.yml

@@ -59,7 +59,16 @@ jobs:
     - name: Archive production files
       id: archive-prod-files
       run: |
-        tar -cf production.tar packages/**/dist packages/app/public
+        tar -cf production.tar \
+          package.json \
+          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
 
     - name: Upload production files as artifact

+ 1 - 1
lerna.json

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

+ 2 - 2
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "5.0.12-RC.0",
+  "version": "5.1.0-RC.2",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -61,7 +61,7 @@
     "eslint": "^8.18.0",
     "eslint-config-next": "^12.1.6",
     "eslint-config-weseek": "^2.1.0",
-    "eslint-import-resolver-typescript": "^3.1.1",
+    "eslint-import-resolver-typescript": "^3.2.5",
     "eslint-plugin-import": "^2.26.0",
     "eslint-plugin-jest": "^26.5.3",
     "eslint-plugin-react": "^7.30.1",

+ 5 - 1
packages/app/.env.development

@@ -7,7 +7,6 @@ MIGRATIONS_DIR=src/migrations/
 APP_SITE_URL=http://localhost:3000
 FILE_UPLOAD=mongodb
 # MONGO_GRIDFS_TOTAL_LIMIT=10485760
-MATHJAX=1
 # NO_CDN=true
 MONGO_URI="mongodb://mongo:27017/growi"
 # REDIS_URI="http://redis:6379"
@@ -29,3 +28,8 @@ OGP_URI="http://ogp:8088"
 # SLACKBOT_WITHOUT_PROXY_BOT_TOKEN=''
 # GROWI_CLOUD_URI='http://growi.cloud'
 # GROWI_APP_ID_FOR_GROWI_CLOUD=012345
+# AUDIT_LOG_ENABLED=false
+# ACTIVITY_EXPIRATION_SECONDS=2592000
+# AUDIT_LOG_ACTION_GROUP_SIZE=SMALL
+# AUDIT_LOG_ADDITIONAL_ACTIONS=
+# AUDIT_LOG_EXCLUDE_ACTIONS=

+ 2 - 2
packages/app/bin/github-actions/update-readme.sh

@@ -2,5 +2,5 @@
 
 cd docker
 
-sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`5\.0\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/docker\/Dockerfile.\+\)$/\1${RELEASED_VERSION}\2\3${RELEASED_VERSION}\4/" README.md
-sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`5\.0-nocdn\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/docker\/Dockerfile.\+\)$/\1${RELEASED_VERSION}-nocdn\2\3${RELEASED_VERSION}\4/" README.md
+sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`5\.1\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/docker\/Dockerfile.\+\)$/\1${RELEASED_VERSION}\2\3${RELEASED_VERSION}\4/" README.md
+sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`5\.1-nocdn\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/docker\/Dockerfile.\+\)$/\1${RELEASED_VERSION}-nocdn\2\3${RELEASED_VERSION}\4/" README.md

+ 83 - 0
packages/app/config/rate-limiter.ts

@@ -0,0 +1,83 @@
+export type IApiRateLimitConfig = {
+  method: string,
+  maxRequests: number,
+  usersPerIpProspection?: number,
+}
+export type IApiRateLimitEndpointMap = {
+  [endpoint: string]: IApiRateLimitConfig
+}
+
+export const DEFAULT_MAX_REQUESTS = 500;
+export const DEFAULT_DURATION_SEC = 60;
+export const DEFAULT_USERS_PER_IP_PROSPECTION = 5;
+
+const MAX_REQUESTS_TIER_1 = 5;
+const MAX_REQUESTS_TIER_2 = 20;
+const MAX_REQUESTS_TIER_3 = 50;
+const MAX_REQUESTS_TIER_4 = 100;
+
+// default config without reg exp
+export const defaultConfig: IApiRateLimitEndpointMap = {
+  '/_api/v3/healthcheck': {
+    method: 'GET',
+    maxRequests: 60,
+    usersPerIpProspection: 1,
+  },
+  '/installer': {
+    method: 'POST',
+    maxRequests: MAX_REQUESTS_TIER_1,
+    usersPerIpProspection: 1,
+  },
+  '/login': {
+    method: 'POST',
+    maxRequests: MAX_REQUESTS_TIER_1,
+    usersPerIpProspection: 100,
+  },
+  '/login/activateInvited': {
+    method: 'POST',
+    maxRequests: MAX_REQUESTS_TIER_2,
+  },
+  '/register': {
+    method: 'POST',
+    maxRequests: MAX_REQUESTS_TIER_1,
+    usersPerIpProspection: 20,
+  },
+  '/user-activation/register': {
+    method: 'POST',
+    maxRequests: MAX_REQUESTS_TIER_1,
+    usersPerIpProspection: 20,
+  },
+  '/_api/login/testLdap': {
+    method: 'POST',
+    maxRequests: MAX_REQUESTS_TIER_2,
+    usersPerIpProspection: 1,
+  },
+  '/_api/check_username': {
+    method: 'GET',
+    maxRequests: MAX_REQUESTS_TIER_3,
+  },
+};
+
+// default config with reg exp
+export const defaultConfigWithRegExp = {
+  '/forgot-password/.*': {
+    method: 'ALL',
+    maxRequests: MAX_REQUESTS_TIER_1,
+  },
+  '/user-activation/.*': {
+    method: 'GET',
+    maxRequests: MAX_REQUESTS_TIER_1,
+  },
+  '/attachment/[0-9a-z]{24}': {
+    method: 'GET',
+    maxRequests: MAX_REQUESTS_TIER_4,
+  },
+  '/download/[0-9a-z]{24}': {
+    method: 'GET',
+    maxRequests: MAX_REQUESTS_TIER_4,
+  },
+  '/share/[0-9a-z]{24}': {
+    method: 'GET',
+    maxRequests: MAX_REQUESTS_TIER_4,
+  },
+};

+ 0 - 3
packages/app/docker/Dockerfile

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

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

@@ -10,12 +10,12 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 
-* [`5.0.11`, `5.0`, `5`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.11/docker/Dockerfile)
-* [`5.0.11-nocdn`, `5.0-nocdn`, `5-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.11/docker/Dockerfile)
-* [`4.5.22`, `4.5`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.22/docker/Dockerfile)
-* [`4.5.22-nocdn`, `4.5-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.22/docker/Dockerfile)
-* [`4.4.13`, `4.4` (Dockerfile)](https://github.com/weseek/growi/blob/v4.4.13/docker/Dockerfile)
-* [`4.4.13-nocdn`, `4.4-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.4.13/docker/Dockerfile)
+* [`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.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)
+* [`4.5.23-nocdn`, `4.5-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.23/packages/app/docker/Dockerfile)
 
 
 What is GROWI?

+ 24 - 4
packages/app/next.config.js

@@ -3,7 +3,7 @@ import { I18NextHMRPlugin } from 'i18next-hmr/plugin';
 import { WebpackManifestPlugin } from 'webpack-manifest-plugin';
 
 import { i18n, localePath } from './src/next-i18next.config';
-import { listScopedPackages } from './src/utils/next.config.utils';
+import { listScopedPackages, listPrefixedPackages } from './src/utils/next.config.utils';
 
 
 // setup logger
@@ -15,12 +15,28 @@ const logger = eazyLogger.Logger({
 
 const setupWithTM = () => {
   // define transpiled packages for '@growi/*'
-  const scopedPackages = listScopedPackages(['@growi'], { ignorePackageNames: '@growi/app' });
+  const packages = [
+    ...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-']),
+  ];
 
   logger.info('{bold:Listing scoped packages for transpiling:}');
-  logger.unprefixed('info', `{grey:${JSON.stringify(scopedPackages, null, 2)}}`);
+  logger.unprefixed('info', `{grey:${JSON.stringify(packages, null, 2)}}`);
 
-  return require('next-transpile-modules')(scopedPackages);
+  return require('next-transpile-modules')(packages);
 };
 const withTM = setupWithTM();
 
@@ -33,6 +49,10 @@ const additionalWebpackEntries = {
 
 /** @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',

+ 23 - 21
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "5.0.12-RC.0",
+  "version": "5.1.0-RC.2",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -13,7 +13,7 @@
     "build:server": "yarn cross-env NODE_ENV=production tsc -p tsconfig.build.server.json && tsc-alias -p tsconfig.build.server.json",
     "clean": "npx -y shx rm -rf dist transpiled",
     "prebuild": "yarn cross-env NODE_ENV=production run-p clean resources:*",
-    "postbuild": "npx -y shx mv transpiled/src dist && npx -y shx cp -r src/server/views dist/server/ && npx -y shx rm -rf transpiled",
+    "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",
@@ -63,11 +63,11 @@
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^5.0.12-RC.0",
-    "@growi/plugin-attachment-refs": "^5.0.12-RC.0",
-    "@growi/plugin-lsx": "^5.0.12-RC.0",
-    "@growi/plugin-pukiwiki-like-linker": "^5.0.12-RC.0",
-    "@growi/slack": "^5.0.12-RC.0",
+    "@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",
     "@promster/express": "^7.0.2",
     "@promster/server": "^7.0.4",
     "@slack/events-api": "^3.0.0",
@@ -102,7 +102,6 @@
     "express": "^4.16.1",
     "express-bunyan-logger": "^1.3.3",
     "express-mongo-sanitize": "^2.1.0",
-    "express-rate-limit": "^5.3.0",
     "express-session": "^1.16.1",
     "express-validator": "^6.14.0",
     "express-webpack-assets": "^0.1.0",
@@ -127,6 +126,7 @@
     "multer-autoreap": "^1.0.3",
     "next": "^12.1.6",
     "next-i18next": "^11.0.0",
+    "next-themes": "^0.2.0",
     "next-transpile-modules": "^9.0.0",
     "nocache": "^3.0.1",
     "nodemailer": "^6.6.2",
@@ -142,15 +142,26 @@
     "passport-saml": "^3.2.0",
     "passport-twitter": "^1.0.4",
     "prom-client": "^13.0.0",
+    "rate-limiter-flexible": "^2.3.7",
     "react": "^18.2.0",
     "react-card-flip": "^1.0.10",
+    "react-datepicker": "^4.7.0",
     "react-dnd": "^14.0.5",
     "react-dnd-html5-backend": "^14.1.0",
     "react-dom": "^18.2.0",
     "react-image-crop": "^8.3.0",
+    "react-markdown": "^8.0.3",
     "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",
@@ -170,7 +181,8 @@
     "handsontable": "v7.0.0 or above is no loger MIT lisence."
   },
   "devDependencies": {
-    "@growi/ui": "^5.0.12-RC.0",
+    "@alienfast/i18next-loader": "^1.1.4",
+    "@growi/ui": "^5.1.0-RC.2",
     "@handsontable/react": "=2.1.0",
     "@types/compression": "^1.7.0",
     "@types/express": "^4.17.11",
@@ -191,6 +203,7 @@
     "emoji-mart": "npm:panta82-emoji-mart@^3.0.1",
     "eslint-plugin-cypress": "^2.12.1",
     "eslint-plugin-regex": "^1.8.0",
+    "font-awesome": "^4.7.0",
     "handsontable": "=6.2.2",
     "i18next-hmr": "^1.7.7",
     "jquery-slimscroll": "^1.3.8",
@@ -198,17 +211,6 @@
     "jquery.cookie": "~1.4.1",
     "jshint": "^2.13.0",
     "load-css-file": "^1.0.0",
-    "markdown-it": "^10.0.0",
-    "markdown-it-blockdiag": "^1.1.1",
-    "markdown-it-drawio-viewer": "^1.3.1",
-    "markdown-it-emoji": "^1.4.0",
-    "markdown-it-emoji-mart": "^0.1.1",
-    "markdown-it-footnote": "^3.0.1",
-    "markdown-it-mathjax": "^2.0.0",
-    "markdown-it-named-headers": "^0.0.4",
-    "markdown-it-plantuml": "^1.3.0",
-    "markdown-it-task-checkbox": "^1.0.6",
-    "markdown-it-toc-and-anchor-with-slugid": "^1.1.4",
     "markdown-table": "^1.1.1",
     "material-icons": "^1.11.3",
     "morgan": "^1.10.0",
@@ -234,7 +236,7 @@
     "socket.io-client": "^4.2.0",
     "sticky-events": "^3.4.11",
     "swagger2openapi": "^5.3.1",
-    "swr": "^1.1.2",
+    "swr": "^1.3.0",
     "throttle-debounce": "^3.0.1",
     "toastr": "^2.1.2",
     "ts-node-dev": "^2.0.0",

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

@@ -521,5 +521,188 @@
       "force_update_parents_label": "Forcibly add missing users",
       "force_update_parents_description": "Enable this option to force the addition of missing users to the ancestor groups if they exist after changing a parent group."
     }
+  },
+  "audit_log_management": {
+    "username": "Username",
+    "date": "Date",
+    "action": "Action",
+    "ip": "IP Address",
+    "url": "URL",
+    "settings": "Settings",
+    "return": "Return",
+    "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>.",
+    "available_action_list": "Search / View All Available Actions",
+    "available_action_list_explain": "List of actions that can be search / view in the Audit Log",
+    "action_list": "Action List",
+    "disable_mode_explain": "Audit log is currently disabled. To enable it, set the environment variable <code>AUDIT_LOG_ENABLED</code> to true."
+  },
+  "audit_log_action_category": {
+    "Page": "Page",
+    "Comment": "Comment",
+    "Tag": "Tag",
+    "Attachment": "Attachment",
+    "ShareLink": "ShareLink",
+    "Search": "Search",
+    "User": "User",
+    "Admin": "Admin"
+  },
+  "audit_log_action": {
+    "USER_REGISTRATION_SUCCESS": "User Creation",
+    "USER_LOGIN_WITH_LOCAL": "Login with ID/Password",
+    "USER_LOGIN_WITH_LDAP": "Login with LDAP",
+    "USER_LOGIN_WITH_GOOGLE": "Login with Google",
+    "USER_LOGIN_WITH_GITHUB": "Login with GitHub",
+    "USER_LOGIN_WITH_TWITTER": "Login with Twitter",
+    "USER_LOGIN_WITH_OIDC": "Login with OIDC",
+    "USER_LOGIN_WITH_SAML": "Login with SAML",
+    "USER_LOGIN_WITH_BASIC": "Login with BASIC",
+    "USER_LOGIN_FAILURE": "Login failure",
+    "USER_LOGOUT": "Logout",
+    "USER_FOGOT_PASSWORD": "Request password reset",
+    "USER_RESET_PASSWORD": "Reset password",
+    "USER_PERSONAL_SETTINGS_UPDATE": "User personal settings update",
+    "USER_IMAGE_TYPE_UPDATE": "User image type update",
+    "USER_LDAP_ACCOUNT_ASSOCIATE": "LDAP account associate",
+    "USER_LDAP_ACCOUNT_DISCONNECT": "LDAP account disconnect",
+    "USER_PASSWORD_UPDATE": "Password update",
+    "USER_API_TOKEN_UPDATE": "API Token update",
+    "USER_EDITOR_SETTINGS_UPDATE": "Editor settings update",
+    "USER_IN_APP_NOTIFICATION_SETTINGS_UPDATE": "In-App Notification settings update",
+    "PAGE_VIEW": "Page view",
+    "PAGE_USER_HOME_VIEW": "Page view (User home)",
+    "PAGE_FORBIDDEN": "Page view (Fobidden page)",
+    "PAGE_NOT_FOUND": "Page view (Not found page)",
+    "PAGE_NOT_CREATABLE": "Page view(Not Creatable page)",
+    "PAGE_LIKE": "Like",
+    "PAGE_UNLIKE": "Unlike",
+    "PAGE_BOOKMARK": "Bookmark",
+    "PAGE_UNBOOKMARK": "Unbookmark",
+    "PAGE_CREATE": "Create page",
+    "PAGE_UPDATE": "Update page",
+    "PAGE_RENAME": "Rename page",
+    "PAGE_DUPLICATE": "Duplicate page",
+    "PAGE_DELETE": "Delete page",
+    "PAGE_DELETE_COMPLETELY": "Delete completely page",
+    "PAGE_REVERT": "Revert page",
+    "PAGE_EMPTY_TRASH": "Empty trash",
+    "PAGE_SUBSCRIBE": "Subscribe page",
+    "PAGE_UNSUBSCRIBE": "Unsubscribe page",
+    "PAGE_EXPORT": "Export page",
+    "TAG_UPDATE": "Update tags",
+    "IN_APP_NOTIFICATION_ALL_STATUSES_OPEN": "Read all In-App notifications",
+    "COMMENT_CREATE": "Create comment",
+    "COMMENT_UPDATE": "Update comment",
+    "COMMENT_REMOVE": "Delete comment",
+    "SHARE_LINK_CREATE": "Create Share link",
+    "SHARE_LINK_DELETE": "Delete Share link",
+    "SHARE_LINK_DELETE_BY_PAGE": "Remove all shared links on the page",
+    "SHARE_LINK_ALL_DELETE": "Delete all share link",
+    "SHARE_LINK_PAGE_VIEW": "Page view(Share link)",
+    "SHARE_LINK_EXPIRED_PAGE_VIEW": "Page view(Expired share link)",
+    "SHARE_LINK_NOT_FOUND": "Page view (Not found share link)",
+    "ATTACHMENT_ADD": "Add Attachment",
+    "ATTACHMENT_REMOVE": "Delete Attachment",
+    "ACTION_ATTACHMENT_DOWNLOAD": "Download Attachment",
+    "SEARCH_PAGE": "Page Search",
+    "SEARCH_PAGE_VIEW": "Page view(Search results page)",
+    "ADMIN_APP_SETTING_UPDATE": "Update App Settings",
+    "ADMIN_SITE_URL_UPDATE": "Update Site URL Settings",
+    "ADMIN_MAIL_SMTP_UPDATE": "Update E-mail(SMTP) Settings",
+    "ADMIN_MAIL_SES_UPDATE": "Update E-mail(SES) Settings",
+    "ADMIN_MAIL_TEST_SUBMIT" : "Send test mail",
+    "ADMIN_FILE_UPLOAD_CONFIG_UPDATE": "Update File Upload Settings",
+    "ADMIN_PLUGIN_UPDATE": "Update Plugin Settings",
+    "ADMIN_MAINTENANCEMODE_ENABLED": "Enable Maintenance Mode",
+    "ADMIN_MAINTENANCEMODE_DISABLED": "Disabled Maintenance Mode",
+    "ADMIN_SECURITY_SETTINGS_UPDATE": "Update Security Settings",
+    "ADMIN_PERMIT_SHARE_LINK": "Enable Share Link",
+    "ADMIN_REJECT_SHARE_LINK": "Disable Share Link",
+    "ADMIN_AUTH_ID_PASS_ENABLED": "Enable ID/Password auth",
+    "ADMIN_AUTH_ID_PASS_DISABLED": "Disable ID/Password auth",
+    "ADMIN_AUTH_ID_PASS_UPDATE": "Update ID/Password auth settings",
+    "ADMIN_AUTH_LDAP_ENABLED": "Enable LDAP auth",
+    "ADMIN_AUTH_LDAP_DISABLED": "Disable LDAP auth",
+    "ADMIN_AUTH_LDAP_UPDATE": "Update LDAP auth settings",
+    "ADMIN_AUTH_SAML_ENABLED": "Enable SAML auth",
+    "ADMIN_AUTH_SAML_DISABLED": "Disable SAML auth",
+    "ADMIN_AUTH_SAML_UPDATE": "Update SAML auth settings",
+    "ADMIN_AUTH_OIDC_ENABLED": "Enable OIDC auth",
+    "ADMIN_AUTH_OIDC_DISABLED": "Disable OIDC auth",
+    "ADMIN_AUTH_OIDC_UPDATE": "Update OIDC settings",
+    "ADMIN_AUTH_BASIC_ENABLED": "Enable BASIC auth",
+    "ADMIN_AUTH_BASIC_DISABLED": "Disable BASIC auth",
+    "ADMIN_AUTH_BASIC_UPDATE": "Update BASIC auth settings",
+    "ADMIN_AUTH_GOOGLE_ENABLED": "Enable Google auth",
+    "ADMIN_AUTH_GOOGLE_DISABLED": "Disable Google auth",
+    "ADMIN_AUTH_GOOGLE_UPDATE": "Update Google auth settings",
+    "ADMIN_AUTH_GITHUB_ENABLED": "Enable GitHub auth",
+    "ADMIN_AUTH_GITHUB_DISABLED": "Disable GitHub auth",
+    "ADMIN_AUTH_GITHUB_UPDATE": "Update GitHub auth settings",
+    "ADMIN_AUTH_TWITTER_ENABLED": "Enable Twitter auth",
+    "ADMIN_AUTH_TWITTER_DISABLED": "Disable Twitter auth",
+    "ADMIN_AUTH_TWITTER_UPDATE": "Update Twitter auth settings",
+    "ADMIN_MARKDOWN_LINE_BREAK_UPDATE": "Update Link Break settings",
+    "ADMIN_MARKDOWN_INDENT_UPDATE": "Update Indent settings",
+    "ADMIN_MARKDOWN_PRESENTATION_UPDATE": "Update Presentation setting",
+    "ADMIN_MARKDOWN_XSS_UPDATE": "Update prevent XSS settings",
+    "ADMIN_LAYOUT_UPDATE": "Update Layout",
+    "ADMIN_THEME_UPDATE": "Update Theme",
+    "ADMIN_SIDEBAR_UPDATE": "Update Default Sidebar mode",
+    "ADMIN_FUNCTION_UPDATE": "Update Function",
+    "ADMIN_CODE_HIGHLIGHT_UPDATE": "Update Code Highlight",
+    "ADMIN_CUSTOM_TITLE_UPDATE": "Update Custom Title",
+    "ADMIN_CUSTOM_HTML_HEADER_UPDATE": "Update Custom HTML header",
+    "ADMIN_CUSTOM_CSS_UPDATE": "Update Custom CSS",
+    "ADMIN_CUSTOM_SCRIPT_UPDATE": "Update Custom script",
+    "ADMIN_ARCHIVE_DATA_UPLOAD": "Upload Archived Data",
+    "ADMIN_GROWI_DATA_IMPORTED": "Import Archived Data",
+    "ADMIN_UPLOADED_GROWI_DATA_DISCARDED": "Discard Archived Data",
+    "ADMIN_ESA_DATA_IMPORTED": "Import from esa.io",
+    "ADMIN_ESA_DATA_UPDATED": "Update esa.io import settings",
+    "ADMIN_CONNECTION_TEST_OF_ESA_DATA": "Test connection to esa",
+    "ADMIN_QIITA_DATA_IMPORTED": "Import from Qiita:Team",
+    "ADMIN_QIITA_DATA_UPDATED": "Update Qiita:Team import settings",
+    "ADMIN_CONNECTION_TEST_OF_QIITA_DATA": "Test connection to Qiita:Team",
+    "ADMIN_ARCHIVE_DATA_CREATE": "Create Archived Data",
+    "ADMIN_ARCHIVE_DATA_DOWNLOAD": "Download Archive Data",
+    "ADMIN_ARCHIVE_DATA_DELETE": "Delete Archive Data",
+    "ADMIN_USER_NOTIFICATION_SETTINGS_ADD": "Add User trigger notification notification settings",
+    "ADMIN_USER_NOTIFICATION_SETTINGS_DELETE": "Delete User trigger notification notification settings",
+    "ADMIN_GLOBAL_NOTIFICATION_SETTINGS_ADD": "Add Grobal notification settings",
+    "ADMIN_GLOBAL_NOTIFICATION_SETTINGS_UPDATE": "Update Grobal notification settings",
+    "ADMIN_NOTIFICATION_GRANT_SETTINGS_UPDATE": "Update Grobal notification permissions",
+    "ADMIN_GLOBAL_NOTIFICATION_SETTINGS_ENABLED": "Enable Grobal notification settings",
+    "ADMIN_GLOBAL_NOTIFICATION_SETTINGS_DISABLED": "Disable Grobal notification settings",
+    "ADMIN_GLOBAL_NOTIFICATION_SETTINGS_DELETE": "Delete Grobal notification settings",
+    "ADMIN_SLACK_WORKSPACE_CREATE": "Add Slack Workspace",
+    "ADMIN_SLACK_WORKSPACE_DELETE": "Delete Slack Workspace",
+    "ADMIN_SLACK_BOT_TYPE_UPDATE": "Change Slack bot type",
+    "ADMIN_SLACK_BOT_TYPE_DELETE": "Delete Slack bot type",
+    "ADMIN_SLACK_ACCESS_TOKEN_REGENERATE": "Regenerate Slack access token",
+    "ADMIN_SLACK_MAKE_APP_PRIMARY": "Make the Slack bot primary",
+    "ADMIN_SLACK_PERMISSION_UPDATE": "Update Slack bot permissions",
+    "ADMIN_SLACK_PROXY_URI_UPDATE": "Update Proxy URL for Custom bot with proxy",
+    "ADMIN_SLACK_RELATION_TEST": "Test connection to slack bot",
+    "ADMIN_SLACK_WITHOUT_PROXY_SETTINGS_UPDATE": "Update Slack bot without proxy settings",
+    "ADMIN_SLACK_WITHOUT_PROXY_PERMISSION_UPDATE": "Update Slack bot without proxy permissions",
+    "ADMIN_SLACK_WITHOUT_PROXY_TEST": "Test connection to Slack bot without proxy",
+    "ADMIN_SLACK_CONFIGURATION_SETTING_UPDATE": "Update Slack Incoming Webhooks configuration",
+    "ADMIN_USERS_INVITE": "User Invitation",
+    "ADMIN_USERS_PASSWORD_RESET": "Reset user password",
+    "ADMIN_USERS_ACTIVATE": "Activate user",
+    "ADMIN_USERS_DEACTIVATE": "Deactivate user",
+    "ADMIN_USERS_GIVE_ADMIN": "Give admin access",
+    "ADMIN_USERS_REMOVE_ADMIN": "Remove admin access",
+    "ADMIN_USERS_SEND_INVITATION_EMAIL": "Resend invitation email",
+    "ADMIN_USERS_REMOVE": "Remove user",
+    "ADMIN_USER_GROUP_CREATE": "Create User Group",
+    "ADMIN_USER_GROUP_UPDATE": "Update User Group",
+    "ADMIN_USER_GROUP_DELETE": "Delete User Group",
+    "ADMIN_USER_GROUP_ADD_USER": "Add User to User Group",
+    "ADMIN_SEARCH_CONNECTION": "Attempting to reconnect to Elasticsearch",
+    "ADMIN_SEARCH_INDICES_NORMALIZE": "Normalize of Elasticsearch indexes",
+    "ADMIN_SEARCH_INDICES_REBUILD": "Rebuild Elasticsearch indexes"
   }
 }

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

@@ -1,4 +1,7 @@
 {
+  "meta": {
+    "display_name": "English"
+  },
   "Help": "Help",
   "view": "View",
   "Edit": "Edit",
@@ -126,6 +129,8 @@
   "UserGroup": "UserGroup",
   "ChildUserGroup": "ChildUserGroup",
   "UserGroup Management": "UserGroup Management",
+  "AuditLog": "Audit Log",
+  "AuditLog Settings": "Audit Log Settings",
   "Full Text Search Management": "Full Text Search Management",
   "Import Data": "Import Data",
   "Export Archive Data": "Export Archive Data",
@@ -193,6 +198,9 @@
     "page_not_exist": "This page does not exist.",
     "page_not_exist_alert": "This page does not exist. Please create a new page."
   },
+  "not_creatable_page": {
+    "could_not_creata_path": "Couldn't create path."
+  },
   "custom_navigation": {
     "no_page_list": "There are no pages under this page.",
     "link_sharing_is_disabled": "Link sharing is disabled."

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

@@ -1,2 +0,0 @@
-// !!DO NOT EDIT/REMOVE THIS FILE!!
-// entry point for @alienfast/i18next-loader

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

@@ -520,5 +520,188 @@
       "force_update_parents_label": "強制的に足りないユーザーを追加する",
       "force_update_parents_description": "このオプションを有効化すると、親グループ変更後に祖先グループに足りないユーザーが存在した場合にそれらのユーザーを強制的に追加することができます"
     }
+  },
+  "audit_log_management": {
+    "username": "ユーザー名",
+    "date": "日付",
+    "action": "アクション",
+    "ip": "IPアドレス",
+    "url": "URL",
+    "settings": "設定",
+    "return": "戻る",
+    "activity_expiration_date": "監査ログの有効期限",
+    "activity_expiration_date_explain": "作成された監査ログは、作成時間から環境変数に設定した秒数後に自動的に削除されます",
+    "fixed_by_env_var": "環境変数により固定されています <code>{{key}}={{value}}</code>.",
+    "available_action_list": "検索 / 表示 可能なアクション一覧",
+    "available_action_list_explain": "監査ログで 検索 / 表示 可能なアクション一覧です",
+    "action_list": "アクション一覧",
+    "disable_mode_explain": "現在、監査ログは無効になっています。有効にする場合は環境変数 <code>AUDIT_LOG_ENABLED</code> を true に設定してください。"
+  },
+  "audit_log_action_category": {
+    "Page": "ページ",
+    "Comment": "コメント",
+    "Tag": "タグ",
+    "Attachment": "添付ファイル",
+    "ShareLink": "シェアリンク",
+    "Search": "検索",
+    "User": "ユーザー",
+    "Admin": "管理者ユーザー"
+  },
+  "audit_log_action": {
+    "USER_REGISTRATION_SUCCESS": "ユーザー作成",
+    "USER_LOGIN_WITH_LOCAL": "ID/Password 認証でログイン",
+    "USER_LOGIN_WITH_LDAP": "LDAP 認証でログイン",
+    "USER_LOGIN_WITH_GOOGLE": "Google 認証でログイン",
+    "USER_LOGIN_WITH_GITHUB": "GitHub 認証でログイン",
+    "USER_LOGIN_WITH_TWITTER": "Twitter 認証でログイン",
+    "USER_LOGIN_WITH_OIDC": "OIDC 認証でログイン",
+    "USER_LOGIN_WITH_SAML": "SAML 認証でログイン",
+    "USER_LOGIN_WITH_BASIC": "BASIC 認証でログイン",
+    "USER_LOGIN_FAILURE": "ログイン失敗",
+    "USER_LOGOUT": "ログアウト",
+    "USER_FOGOT_PASSWORD": "パスワードリセットのリクエスト",
+    "USER_RESET_PASSWORD": "パスワードのリセット",
+    "USER_PERSONAL_SETTINGS_UPDATE": "ユーザーの基本情報の更新",
+    "USER_IMAGE_TYPE_UPDATE": "プロフィール画像の変更",
+    "USER_LDAP_ACCOUNT_ASSOCIATE": "LDAP アカウントの追加",
+    "USER_LDAP_ACCOUNT_DISCONNECT": "LDAP アカウントの切断",
+    "USER_PASSWORD_UPDATE": "パスワードの変更",
+    "USER_API_TOKEN_UPDATE": "API トークンの更新",
+    "USER_EDITOR_SETTINGS_UPDATE": "エディター設定の更新",
+    "USER_IN_APP_NOTIFICATION_SETTINGS_UPDATE": "アプリ内通知設定の更新",
+    "PAGE_VIEW": "ページ閲覧",
+    "PAGE_USER_HOME_VIEW": "ページ閲覧(ユーザーホーム)",
+    "PAGE_FORBIDDEN": "ページ閲覧(fobiddenページ)",
+    "PAGE_NOT_FOUND": "ページ閲覧(Not Found ページ)",
+    "PAGE_NOT_CREATABLE": "ページ閲覧(Not Creatable ページ)",
+    "PAGE_LIKE": "いいね",
+    "PAGE_UNLIKE": "いいねの解除",
+    "PAGE_BOOKMARK": "ブックマーク",
+    "PAGE_UNBOOKMARK": "ブックマークの解除",
+    "PAGE_CREATE": "ページの作成",
+    "PAGE_UPDATE": "ページの更新",
+    "PAGE_RENAME": "ページのリネーム",
+    "PAGE_DUPLICATE": "ページの複製",
+    "PAGE_DELETE": "ページの削除",
+    "PAGE_DELETE_COMPLETELY": "ページの完全削除",
+    "PAGE_REVERT": "ページを元に戻す",
+    "PAGE_EMPTY_TRASH": "ゴミ箱を空にする",
+    "PAGE_SUBSCRIBE": "ページをサブスクライブ",
+    "PAGE_UNSUBSCRIBE": "ページをアンサブスクライブ",
+    "PAGE_EXPORT": "マークダウン形式でページをエクスポート",
+    "TAG_UPDATE": "タグの更新",
+    "IN_APP_NOTIFICATION_ALL_STATUSES_OPEN": "アプリ内通知を全て既読",
+    "COMMENT_CREATE": "コメントの作成",
+    "COMMENT_UPDATE": "コメントの更新",
+    "COMMENT_REMOVE": "コメントの削除",
+    "SHARE_LINK_CREATE": "共有リンクの作成",
+    "SHARE_LINK_DELETE": "共有リンクの削除",
+    "SHARE_LINK_DELETE_BY_PAGE": "ページ内の共有リンクを全て削除",
+    "SHARE_LINK_ALL_DELETE": "共有リンクを全て削除",
+    "SHARE_LINK_PAGE_VIEW": "ページ閲覧(共有リンク)",
+    "SHARE_LINK_EXPIRED_PAGE_VIEW": "ページ閲覧(期限切れの共有リンク)",
+    "SHARE_LINK_NOT_FOUND": "ページ閲覧(存在しない共有リンク)",
+    "ATTACHMENT_ADD": "添付データの追加",
+    "ATTACHMENT_REMOVE": "添付データの削除",
+    "ACTION_ATTACHMENT_DOWNLOAD": "添付データのダウンロード",
+    "SEARCH_PAGE": "ページの検索",
+    "SEARCH_PAGE_VIEW": "ページ閲覧(検索結果ページ)",
+    "ADMIN_APP_SETTING_UPDATE": "アプリ設定の更新",
+    "ADMIN_SITE_URL_UPDATE": "サイトURL設定の更新",
+    "ADMIN_MAIL_SMTP_UPDATE": "メール設定(SMTP)の更新",
+    "ADMIN_MAIL_SES_UPDATE": "メール設定(SES)の更新",
+    "ADMIN_MAIL_TEST_SUBMIT" : "テストメールの送信",
+    "ADMIN_FILE_UPLOAD_CONFIG_UPDATE": "ファイルアップロード設定の更新",
+    "ADMIN_PLUGIN_UPDATE": "プラグイン設定の更新",
+    "ADMIN_MAINTENANCEMODE_ENABLED": "メンテナンスモードの開始",
+    "ADMIN_MAINTENANCEMODE_DISABLED": "メンテナンスモードの終了",
+    "ADMIN_SECURITY_SETTINGS_UPDATE": "セキュリティ設定の更新",
+    "ADMIN_PERMIT_SHARE_LINK": "共有リンクの有効化",
+    "ADMIN_REJECT_SHARE_LINK": "共有リンクの無効化",
+    "ADMIN_AUTH_ID_PASS_ENABLED": "ID/Password 認証を有効",
+    "ADMIN_AUTH_ID_PASS_DISABLED": "ID/Password 認証を無効",
+    "ADMIN_AUTH_ID_PASS_UPDATE": "ID/Password 認証設定を更新",
+    "ADMIN_AUTH_LDAP_ENABLED": "LDAP 認証を有効",
+    "ADMIN_AUTH_LDAP_DISABLED": "LDAP 認証を無効",
+    "ADMIN_AUTH_LDAP_UPDATE": "LDAP 認証設定を更新",
+    "ADMIN_AUTH_SAML_ENABLED": "SAML 認証を有効",
+    "ADMIN_AUTH_SAML_DISABLED": "SAML 認証を無効",
+    "ADMIN_AUTH_SAML_UPDATE": "SAML 認証設定を更新",
+    "ADMIN_AUTH_OIDC_ENABLED": "OIDC 認証を有効",
+    "ADMIN_AUTH_OIDC_DISABLED": "OIDC 認証を無効",
+    "ADMIN_AUTH_OIDC_UPDATE": "OIDC 認証設定の更新",
+    "ADMIN_AUTH_BASIC_ENABLED": "BASIC 認証の有効",
+    "ADMIN_AUTH_BASIC_DISABLED": "BASIC 認証の無効",
+    "ADMIN_AUTH_BASIC_UPDATE": "BASIC 認証設定の更新",
+    "ADMIN_AUTH_GOOGLE_ENABLED": "Google 認証の有効",
+    "ADMIN_AUTH_GOOGLE_DISABLED": "Google 認証の無効",
+    "ADMIN_AUTH_GOOGLE_UPDATE": "Google 認証設定の更新",
+    "ADMIN_AUTH_GITHUB_ENABLED": "GitHub 認証の有効",
+    "ADMIN_AUTH_GITHUB_DISABLED": "GitHub 認証の無効",
+    "ADMIN_AUTH_GITHUB_UPDATE": "GitHub 認証設定の更新",
+    "ADMIN_AUTH_TWITTER_ENABLED": "Twitter 認証の有効",
+    "ADMIN_AUTH_TWITTER_DISABLED": "Twitter 認証の無効",
+    "ADMIN_AUTH_TWITTER_UPDATE": "Twitter 認証設定の更新",
+    "ADMIN_MARKDOWN_LINE_BREAK_UPDATE": "Line Break 設定の更新",
+    "ADMIN_MARKDOWN_INDENT_UPDATE": "インデント設定の更新",
+    "ADMIN_MARKDOWN_PRESENTATION_UPDATE": "プレゼンテーション設定の更新",
+    "ADMIN_MARKDOWN_XSS_UPDATE": "XSS 対策設定の更新",
+    "ADMIN_LAYOUT_UPDATE": "レイアウト設定の更新",
+    "ADMIN_THEME_UPDATE": "テーマ設定の更新",
+    "ADMIN_SIDEBAR_UPDATE": "デフォルトのサイドバーモードの設定の更新",
+    "ADMIN_FUNCTION_UPDATE": "機能設定の更新",
+    "ADMIN_CODE_HIGHLIGHT_UPDATE": "コードハイライト設定の更新",
+    "ADMIN_CUSTOM_TITLE_UPDATE": "カスタムタイトル設定の更新",
+    "ADMIN_CUSTOM_HTML_HEADER_UPDATE": "カスタム HTML Header 設定の更新",
+    "ADMIN_CUSTOM_CSS_UPDATE": "カスタム CSS 設定の更新",
+    "ADMIN_CUSTOM_SCRIPT_UPDATE": "カスタムスクリプト設定の更新",
+    "ADMIN_ARCHIVE_DATA_UPLOAD": "アーカイブデータのアップロード",
+    "ADMIN_GROWI_DATA_IMPORTED": "アーカイブデータのインポート",
+    "ADMIN_UPLOADED_GROWI_DATA_DISCARDED": "アーカイブデータの破棄",
+    "ADMIN_ESA_DATA_IMPORTED": "esa.io からインポート",
+    "ADMIN_ESA_DATA_UPDATED": "esa.io のインポート設定の更新",
+    "ADMIN_CONNECTION_TEST_OF_ESA_DATA": "esa.io の接続テスト",
+    "ADMIN_QIITA_DATA_IMPORTED": "Qiita:Team からのインポート",
+    "ADMIN_QIITA_DATA_UPDATED": "Qiita:Team のインポート設定の更新",
+    "ADMIN_CONNECTION_TEST_OF_QIITA_DATA": "Qiita:Team の接続テスト",
+    "ADMIN_ARCHIVE_DATA_CREATE": "アーカイブデータの作成",
+    "ADMIN_ARCHIVE_DATA_DOWNLOAD": "アーカイブデータのダウンロード",
+    "ADMIN_ARCHIVE_DATA_DELETE": "アーカイブデータの削除",
+    "ADMIN_USER_NOTIFICATION_SETTINGS_ADD": "User trigger notification の通知設定の追加",
+    "ADMIN_USER_NOTIFICATION_SETTINGS_DELETE": "User trigger notification の通知設定の削除",
+    "ADMIN_GLOBAL_NOTIFICATION_SETTINGS_ADD": "Grobal notification の設定の追加",
+    "ADMIN_GLOBAL_NOTIFICATION_SETTINGS_UPDATE": "Grobal notification の設定の更新",
+    "ADMIN_NOTIFICATION_GRANT_SETTINGS_UPDATE": "Grobal notification の権限の更新",
+    "ADMIN_GLOBAL_NOTIFICATION_SETTINGS_ENABLED": "Grobal notification の設定の有効",
+    "ADMIN_GLOBAL_NOTIFICATION_SETTINGS_DISABLED": "Grobal notification の設定の無効",
+    "ADMIN_GLOBAL_NOTIFICATION_SETTINGS_DELETE": "Grobal notification の設定の削除",
+    "ADMIN_SLACK_WORKSPACE_CREATE": "Slack ワークスペースの追加",
+    "ADMIN_SLACK_WORKSPACE_DELETE": "Slack ワークスペースの削除",
+    "ADMIN_SLACK_BOT_TYPE_UPDATE": "Slack bot タイプの変更",
+    "ADMIN_SLACK_BOT_TYPE_DELETE": "Slack bot タイプの削除",
+    "ADMIN_SLACK_ACCESS_TOKEN_REGENERATE": "Slack アクセストークンの再発行",
+    "ADMIN_SLACK_MAKE_APP_PRIMARY": "Slack bot をプライマリーにする",
+    "ADMIN_SLACK_PERMISSION_UPDATE": "Slack bot の権限の更新",
+    "ADMIN_SLACK_PROXY_URI_UPDATE": "Custom bot with proxy の Proxy URL の更新",
+    "ADMIN_SLACK_RELATION_TEST": "Slack bot の接続テスト",
+    "ADMIN_SLACK_WITHOUT_PROXY_SETTINGS_UPDATE": "Slack bot without proxy の設定の更新",
+    "ADMIN_SLACK_WITHOUT_PROXY_PERMISSION_UPDATE": "Slack bot without proxy の権限の更新",
+    "ADMIN_SLACK_WITHOUT_PROXY_TEST": "Slack bot without proxy の接続テスト",
+    "ADMIN_SLACK_CONFIGURATION_SETTING_UPDATE": "Slack Incoming Webhooks の設定の更新",
+    "ADMIN_USERS_INVITE": "ユーザーの招待",
+    "ADMIN_USERS_PASSWORD_RESET": "ユーザーのパスワードをリセット",
+    "ADMIN_USERS_ACTIVATE": "ユーザーを承認する",
+    "ADMIN_USERS_DEACTIVATE": "ユーザーを停止する",
+    "ADMIN_USERS_GIVE_ADMIN": "管理者にする",
+    "ADMIN_USERS_REMOVE_ADMIN": "管理者から外す",
+    "ADMIN_USERS_SEND_INVITATION_EMAIL": "招待メールの再送信",
+    "ADMIN_USERS_REMOVE": "ユーザーの削除",
+    "ADMIN_USER_GROUP_CREATE": "ユーザーグループの作成",
+    "ADMIN_USER_GROUP_UPDATE": "ユーザーグループの更新",
+    "ADMIN_USER_GROUP_DELETE": "ユーザーグループの削除",
+    "ADMIN_USER_GROUP_ADD_USER": "ユーザーグループにユーザーを追加",
+    "ADMIN_SEARCH_CONNECTION": "Elasticsearch の再接続の試行",
+    "ADMIN_SEARCH_INDICES_NORMALIZE": "Elasticsearch のインデックスの正規化",
+    "ADMIN_SEARCH_INDICES_REBUILD": "Elasticsearch のインデックスのリビルド"
   }
 }

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

@@ -1,4 +1,7 @@
 {
+  "meta": {
+    "display_name": "日本語"
+  },
   "Help": "ヘルプ",
   "view": "View",
   "Edit": "編集",
@@ -126,6 +129,8 @@
   "UserGroup": "グループ",
   "ChildUserGroup": "子グループ",
   "UserGroup Management": "グループ管理",
+  "AuditLog": "監査ログ",
+  "AuditLog Settings": "監査ログ設定",
   "Full Text Search Management": "全文検索管理",
   "Import Data": "データインポート",
   "Export Archive Data": "データアーカイブ",
@@ -195,6 +200,9 @@
     "page_not_exist": "このページは存在しません。",
     "page_not_exist_alert": "このページは存在しません。新たに作成する必要があります。"
   },
+  "not_creatable_page": {
+    "could_not_creata_path": "パスを作成できませんでした。"
+  },
   "custom_navigation": {
     "no_page_list": "このページの配下にはページが存在しません。",
     "link_sharing_is_disabled": "リンクのシェアは無効化されています"

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

@@ -530,5 +530,188 @@
       "force_update_parents_label": "强行添加失踪的用户",
       "force_update_parents_description": "激活这个选项,如果在父组改变后,在祖先组中有缺失的用户,可以强制添加这些用户"
     }
+  },
+  "audit_log_management": {
+    "username": "帐号",
+    "date": "日期",
+    "action": "行动",
+    "ip": "IP地址",
+    "url": "URL",
+    "settings": "设置",
+    "return": "返回",
+    "activity_expiration_date": "审计日志的到期日",
+    "activity_expiration_date_explain": "创建的审计日志会在环境变量中设置的从创建时间算起的秒数后自动删除",
+    "fixed_by_env_var": "这是由env var 修复的 <code>{{key}}={{value}}</code>.",
+    "available_action_list": "搜索/查看 所有可用的行动",
+    "available_action_list_explain": "可以在审计日志中 搜索/查看 的行动列表",
+    "action_list": "行动清单",
+    "disable_mode_explain": "审计日志当前已禁用。 要启用它,请将环境变量 <code>AUDIT_LOG_ENABLED</code> 设置为 true。"
+  },
+  "audit_log_action_category": {
+    "Page": "页面",
+    "Comment": "评论",
+    "Tag": "标签",
+    "Attachment": "附件",
+    "ShareLink": "分享链接",
+    "Search": "搜索",
+    "User": "用户",
+    "Admin": "管理"
+  },
+  "audit_log_action": {
+    "USER_REGISTRATION_SUCCESS": "用户创建",
+    "USER_LOGIN_WITH_LOCAL": "用ID/密码登录",
+    "USER_LOGIN_WITH_LDAP": "使用 LDAP 登录",
+    "USER_LOGIN_WITH_GOOGLE": "用谷歌登录",
+    "USER_LOGIN_WITH_GITHUB": "使用 GitHub 登录",
+    "USER_LOGIN_WITH_TWITTER": "使用 Twitter 登录",
+    "USER_LOGIN_WITH_OIDC": "使用 OIDC 登录",
+    "USER_LOGIN_WITH_SAML": "使用 SAML 登录",
+    "USER_LOGIN_WITH_BASIC": "使用 BASIC 登录",
+    "USER_LOGIN_FAILURE": "登录失败",
+    "USER_LOGOUT": "注销",
+    "USER_FOGOT_PASSWORD": "要求重置密码",
+    "USER_RESET_PASSWORD": "重置密码",
+    "USER_PERSONAL_SETTINGS_UPDATE": "用户个人设置更新",
+    "USER_IMAGE_TYPE_UPDATE": "用户图片类型更新",
+    "USER_LDAP_ACCOUNT_ASSOCIATE": "LDAP 帐户关联",
+    "USER_LDAP_ACCOUNT_DISCONNECT": "LDAP 账户断开连接",
+    "USER_PASSWORD_UPDATE": "密码更新",
+    "USER_API_TOKEN_UPDATE": "API 令牌更新",
+    "USER_EDITOR_SETTINGS_UPDATE": "编辑器设置更新",
+    "USER_IN_APP_NOTIFICATION_SETTINGS_UPDATE": "应用内通知设置更新",
+    "PAGE_VIEW": "页面浏览量",
+    "PAGE_USER_HOME_VIEW": "页面浏览量(用户主页)",
+    "PAGE_FORBIDDEN": "页面浏览量(禁止页面)",
+    "PAGE_NOT_FOUND": "页面查看(未找到页面)",
+    "PAGE_NOT_CREATABLE": "页面浏览量(不可创建页面)",
+    "PAGE_LIKE": "喜欢",
+    "PAGE_UNLIKE": "不喜欢",
+    "PAGE_BOOKMARK": "书签",
+    "PAGE_UNBOOKMARK": "取消书签",
+    "PAGE_CREATE": "创建页面",
+    "PAGE_UPDATE": "更新页面",
+    "PAGE_RENAME": "重命名页面",
+    "PAGE_DUPLICATE": "重复页面",
+    "PAGE_DELETE": "删除页面",
+    "PAGE_DELETE_COMPLETELY": "彻底删除页面",
+    "PAGE_REVERT": "还原页面",
+    "PAGE_EMPTY_TRASH": "清空垃圾箱",
+    "PAGE_SUBSCRIBE": "订阅页面",
+    "PAGE_UNSUBSCRIBE": "退订页面",
+    "PAGE_EXPORT": "导出页面",
+    "TAG_UPDATE": "更新标签",
+    "IN_APP_NOTIFICATION_ALL_STATUSES_OPEN": "读取所有应用内通知",
+    "COMMENT_CREATE": "创建评论",
+    "COMMENT_UPDATE": "更新评论",
+    "COMMENT_REMOVE": "删除评论",
+    "SHARE_LINK_CREATE": "创建分享链接",
+    "SHARE_LINK_DELETE": "删除分享链接",
+    "SHARE_LINK_DELETE_BY_PAGE": "删除页面上的所有共享链接",
+    "SHARE_LINK_ALL_DELETE": "删除所有分享链接",
+    "SHARE_LINK_PAGE_VIEW": "页面浏览量(分享链接)",
+    "SHARE_LINK_EXPIRED_PAGE_VIEW": "页面浏览量(已过期的分享链接)",
+    "SHARE_LINK_NOT_FOUND": "页面浏览量(未找到分享链接)",
+    "ATTACHMENT_ADD": "添加附件",
+    "ATTACHMENT_REMOVE": "删除附件",
+    "ACTION_ATTACHMENT_DOWNLOAD": "下载附件",
+    "SEARCH_PAGE": "页面搜索",
+    "SEARCH_PAGE_VIEW": "页面浏览量(搜索结果页面)",
+    "ADMIN_APP_SETTING_UPDATE": "更新应用设置",
+    "ADMIN_SITE_URL_UPDATE": "更新站点 URL 设置",
+    "ADMIN_MAIL_SMTP_UPDATE": "更新电子邮件(SMTP)设置",
+    "ADMIN_MAIL_SES_UPDATE": "更新电子邮件(SES)设置",
+    "ADMIN_MAIL_TEST_SUBMIT" : "发送测试邮件",
+    "ADMIN_FILE_UPLOAD_CONFIG_UPDATE": "更新文件上传设置",
+    "ADMIN_PLUGIN_UPDATE": "更新插件设置",
+    "ADMIN_MAINTENANCEMODE_ENABLED": "启用维护模式",
+    "ADMIN_MAINTENANCEMODE_DISABLED": "禁用维护模式",
+    "ADMIN_SECURITY_SETTINGS_UPDATE": "更新安全设置",
+    "ADMIN_PERMIT_SHARE_LINK": "启用分享链接",
+    "ADMIN_REJECT_SHARE_LINK": "禁用分享链接",
+    "ADMIN_AUTH_ID_PASS_ENABLED": "启用 ID/密码验证",
+    "ADMIN_AUTH_ID_PASS_DISABLED": "禁用 ID/密码验证",
+    "ADMIN_AUTH_ID_PASS_UPDATE": "更新 ID/密码验证设置",
+    "ADMIN_AUTH_LDAP_ENABLED": "启用 LDAP 身份验证",
+    "ADMIN_AUTH_LDAP_DISABLED": "禁用 LDAP 身份验证",
+    "ADMIN_AUTH_LDAP_UPDATE": "更新 LDAP 身份验证设置",
+    "ADMIN_AUTH_SAML_ENABLED": "启用 SAML 身份验证",
+    "ADMIN_AUTH_SAML_DISABLED": "禁用 SAML 身份验证",
+    "ADMIN_AUTH_SAML_UPDATE": "更新 SAML 身份验证设置",
+    "ADMIN_AUTH_OIDC_ENABLED": "启用 OIDC 身份验证",
+    "ADMIN_AUTH_OIDC_DISABLED": "禁用 OIDC 身份验证",
+    "ADMIN_AUTH_OIDC_UPDATE": "更新 OIDC 设置",
+    "ADMIN_AUTH_BASIC_ENABLED": "启用基本身份验证",
+    "ADMIN_AUTH_BASIC_DISABLED": "禁用基本身份验证",
+    "ADMIN_AUTH_BASIC_UPDATE": "更新基本认证设置",
+    "ADMIN_AUTH_GOOGLE_ENABLED": "启用谷歌身份验证",
+    "ADMIN_AUTH_GOOGLE_DISABLED": "禁用谷歌身份验证",
+    "ADMIN_AUTH_GOOGLE_UPDATE": "更新谷歌授权设置",
+    "ADMIN_AUTH_GITHUB_ENABLED": "启用 GitHub 身份验证",
+    "ADMIN_AUTH_GITHUB_DISABLED": "禁用 GitHub 身份验证",
+    "ADMIN_AUTH_GITHUB_UPDATE": "更新 GitHub 授权设置",
+    "ADMIN_AUTH_TWITTER_ENABLED": "启用 Twitter 身份验证",
+    "ADMIN_AUTH_TWITTER_DISABLED": "禁用 Twitter 身份验证",
+    "ADMIN_AUTH_TWITTER_UPDATE": "更新 Twitter 授权设置",
+    "ADMIN_MARKDOWN_LINE_BREAK_UPDATE": "更新链接中断设置",
+    "ADMIN_MARKDOWN_INDENT_UPDATE": "更新缩进设置",
+    "ADMIN_MARKDOWN_PRESENTATION_UPDATE": "更新演示设置",
+    "ADMIN_MARKDOWN_XSS_UPDATE": "更新阻止 XSS 设置",
+    "ADMIN_LAYOUT_UPDATE": "更新布局",
+    "ADMIN_THEME_UPDATE": "更新主题",
+    "ADMIN_SIDEBAR_UPDATE": "更新默认的侧边栏模式",
+    "ADMIN_FUNCTION_UPDATE": "更新函数",
+    "ADMIN_CODE_HIGHLIGHT_UPDATE": "更新代码高亮",
+    "ADMIN_CUSTOM_TITLE_UPDATE": "更新自定义标题",
+    "ADMIN_CUSTOM_HTML_HEADER_UPDATE": "更新自定义 HTML 标头",
+    "ADMIN_CUSTOM_CSS_UPDATE": "更新自定义 CSS",
+    "ADMIN_CUSTOM_SCRIPT_UPDATE": "更新自定义脚本",
+    "ADMIN_ARCHIVE_DATA_UPLOAD": "上传存档数据",
+    "ADMIN_GROWI_DATA_IMPORTED": "导入存档数据",
+    "ADMIN_UPLOADED_GROWI_DATA_DISCARDED": "丢弃存档数据",
+    "ADMIN_ESA_DATA_IMPORTED": "从 esa.io 导入",
+    "ADMIN_ESA_DATA_UPDATED": "更新 esa.io 导入设置",
+    "ADMIN_CONNECTION_TEST_OF_ESA_DATA": "测试与 esa 的连接",
+    "ADMIN_QIITA_DATA_IMPORTED": "从 Qiita:Team 导入",
+    "ADMIN_QIITA_DATA_UPDATED": "更新 Qiita:团队导入设置",
+    "ADMIN_CONNECTION_TEST_OF_QIITA_DATA": "测试与 Qiita:Team 的连接",
+    "ADMIN_ARCHIVE_DATA_CREATE": "创建归档数据",
+    "ADMIN_ARCHIVE_DATA_DOWNLOAD": "下载存档数据",
+    "ADMIN_ARCHIVE_DATA_DELETE": "删除存档数据",
+    "ADMIN_USER_NOTIFICATION_SETTINGS_ADD": "添加用户触发通知通知设置",
+    "ADMIN_USER_NOTIFICATION_SETTINGS_DELETE": "删除用户触发通知通知设置",
+    "ADMIN_GLOBAL_NOTIFICATION_SETTINGS_ADD": "添加全局通知设置",
+    "ADMIN_GLOBAL_NOTIFICATION_SETTINGS_UPDATE": "更新 Grobal 通知设置",
+    "ADMIN_NOTIFICATION_GRANT_SETTINGS_UPDATE": "更新 Grobal 通知权限",
+    "ADMIN_GLOBAL_NOTIFICATION_SETTINGS_ENABLED": "启用 Grobal 通知设置",
+    "ADMIN_GLOBAL_NOTIFICATION_SETTINGS_DISABLED": "禁用 Grobal 通知设置",
+    "ADMIN_GLOBAL_NOTIFICATION_SETTINGS_DELETE": "删除 Grobal 通知设置",
+    "ADMIN_SLACK_WORKSPACE_CREATE": "添加松弛工作区",
+    "ADMIN_SLACK_WORKSPACE_DELETE": "删除 Slack 工作区",
+    "ADMIN_SLACK_BOT_TYPE_UPDATE": "更改 Slack 机器人类型",
+    "ADMIN_SLACK_BOT_TYPE_DELETE": "删除 Slack 机器人类型",
+    "ADMIN_SLACK_ACCESS_TOKEN_REGENERATE": "重新生成 Slack 访问令牌",
+    "ADMIN_SLACK_MAKE_APP_PRIMARY": "将 Slack 机器人设为主要",
+    "ADMIN_SLACK_PERMISSION_UPDATE": "更新 Slack 机器人权限",
+    "ADMIN_SLACK_PROXY_URI_UPDATE": "使用代理更新自定义机器人的代理 URL",
+    "ADMIN_SLACK_RELATION_TEST": "测试与 slack 机器人的连接",
+    "ADMIN_SLACK_WITHOUT_PROXY_SETTINGS_UPDATE": "在没有代理设置的情况下更新 Slack 机器人",
+    "ADMIN_SLACK_WITHOUT_PROXY_PERMISSION_UPDATE": "更新没有代理权限的 Slack 机器人",
+    "ADMIN_SLACK_WITHOUT_PROXY_TEST": "在没有代理的情况下测试与 Slack 机器人的连接",
+    "ADMIN_SLACK_CONFIGURATION_SETTING_UPDATE": "更新 Slack Incoming Webhooks 配置",
+    "ADMIN_USERS_INVITE": "用户邀请",
+    "ADMIN_USERS_PASSWORD_RESET": "重置用户密码",
+    "ADMIN_USERS_ACTIVATE": "激活用户",
+    "ADMIN_USERS_DEACTIVATE": "停用用户",
+    "ADMIN_USERS_GIVE_ADMIN": "授予管理员访问权限",
+    "ADMIN_USERS_REMOVE_ADMIN": "删除管理员访问权限",
+    "ADMIN_USERS_SEND_INVITATION_EMAIL": "重发邀请函",
+    "ADMIN_USERS_REMOVE": "删除用户",
+    "ADMIN_USER_GROUP_CREATE": "创建用户组",
+    "ADMIN_USER_GROUP_UPDATE": "更新用户组",
+    "ADMIN_USER_GROUP_DELETE": "删除用户组",
+    "ADMIN_USER_GROUP_ADD_USER": "添加用户到用户组",
+    "ADMIN_SEARCH_CONNECTION": "重试Elasticsearch连接",
+    "ADMIN_SEARCH_INDICES_NORMALIZE": "试图重新连接Elasticsearch",
+    "ADMIN_SEARCH_INDICES_REBUILD": "重建 Elasticsearch 索引"
   }
 }

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

@@ -1,4 +1,7 @@
 {
+  "meta": {
+    "display_name": "简体中文"
+  },
   "Help": "帮助",
   "view": "View",
 	"Edit": "编辑",
@@ -134,6 +137,8 @@
   "UserGroup": "用户组",
   "ChildUserGroup": "儿童用户组",
 	"UserGroup Management": "用户组管理",
+  "AuditLog": "审计日志",
+  "AuditLog Settings": "审计日志设置",
 	"Full Text Search Management": "全文搜索管理",
 	"Import Data": "导入数据",
 	"Export Archive Data": "导出主题数据",
@@ -193,6 +198,9 @@
     "page_not_exist": "该页面不存在",
     "page_not_exist_alert": "该页面不存在,请创建一个新页面"
   },
+  "not_creatable_page": {
+    "could_not_creata_path": "无法创建路径"
+  },
   "custom_navigation": {
     "no_page_list": "There are no pages under this page.",
     "link_sharing_is_disabled": "链接共享已被禁用"

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

+ 2 - 0
packages/app/src/client/admin.jsx

@@ -31,6 +31,7 @@ import { swrGlobalConfiguration } from '~/utils/swr-utils';
 
 import AdminHome from '../components/Admin/AdminHome/AdminHome';
 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';
@@ -107,6 +108,7 @@ Object.assign(componentMappings, {
   'admin-user-group-detail': <UserGroupDetailPage />,
   'admin-full-text-search-management': <FullTextSearchManagement />,
   'admin-user-group-page': <UserGroupPage />,
+  'admin-audit-log': <AuditLogManagement />,
   'admin-navigation': <AdminNavigation />,
 });
 

+ 4 - 16
packages/app/src/client/app.jsx

@@ -10,8 +10,7 @@ import { Provider } from 'unstated';
 import ContextExtractor from '~/client/services/ContextExtractor';
 import EditorContainer from '~/client/services/EditorContainer';
 import PageContainer from '~/client/services/PageContainer';
-import PersonalContainer from '~/client/services/PersonalContainer';
-import IdenticalPathPage from '~/components/IdenticalPathPage';
+import { IdenticalPathPage } from '~/components/IdenticalPathPage';
 import PrivateLegacyPages from '~/components/PrivateLegacyPages';
 import loggerFactory from '~/utils/logger';
 import { swrGlobalConfiguration } from '~/utils/swr-utils';
@@ -27,12 +26,10 @@ import MyDraftList from '../components/MyDraftList/MyDraftList';
 import GrowiContextualSubNavigation from '../components/Navbar/GrowiContextualSubNavigation';
 import GrowiSubNavigationSwitcher from '../components/Navbar/GrowiSubNavigationSwitcher';
 import NotFoundPage from '../components/NotFoundPage';
-import Page from '../components/Page';
+import { Page } from '../components/Page';
 import DisplaySwitcher from '../components/Page/DisplaySwitcher';
-import FixPageGrantAlert from '../components/Page/FixPageGrantAlert';
 import RedirectedAlert from '../components/Page/RedirectedAlert';
 import ShareLinkAlert from '../components/Page/ShareLinkAlert';
-import TrashPageAlert from '../components/Page/TrashPageAlert';
 import PageComment from '../components/PageComment';
 import CommentEditorLazyRenderer from '../components/PageComment/CommentEditorLazyRenderer';
 import PageContentFooter from '../components/PageContentFooter';
@@ -57,9 +54,8 @@ const socketIoContainer = appContainer.getContainer('SocketIoContainer');
 // create unstated container instance
 const pageContainer = new PageContainer(appContainer);
 const editorContainer = new EditorContainer(appContainer);
-const personalContainer = new PersonalContainer(appContainer);
 const injectableContainers = [
-  appContainer, socketIoContainer, pageContainer, editorContainer, personalContainer,
+  appContainer, socketIoContainer, pageContainer, editorContainer,
 ];
 
 logger.info('unstated containers have been initialized');
@@ -85,8 +81,6 @@ Object.assign(componentMappings, {
 
   'maintenance-mode-content': <MaintenanceModeContent />,
 
-  'trash-page-alert': <TrashPageAlert />,
-
   'trash-page-list-container': <TrashPageList />,
 
   'not-found-page': <NotFoundPage />,
@@ -95,8 +89,7 @@ Object.assign(componentMappings, {
 
   'page-timeline': <PageTimeline />,
 
-  'personal-setting': <PersonalSettings crowi={personalContainer} />,
-
+  'personal-setting': <PersonalSettings />,
   'my-drafts': <MyDraftList />,
 
   'grw-fab-container': <Fab />,
@@ -119,11 +112,6 @@ if (pageContainer.state.pageId != null) {
 
     'recent-created-icon': <RecentlyCreatedIcon />,
   });
-  if (!pageContainer.state.isEmpty) {
-    Object.assign(componentMappings, {
-      'fix-page-grant-alert': <FixPageGrantAlert />,
-    });
-  }
 }
 if (pageContainer.state.creator != null) {
   Object.assign(componentMappings, {

+ 2 - 10
packages/app/src/client/base.jsx

@@ -3,19 +3,17 @@ import React from 'react';
 import EventEmitter from 'events';
 
 import AppContainer from '~/client/services/AppContainer';
-import SocketIoContainer from '~/client/services/SocketIoContainer';
 import { DescendantsPageListModal } from '~/components/DescendantsPageListModal';
 import PutbackPageModal from '~/components/PutbackPageModal';
 import ShortcutsModal from '~/components/ShortcutsModal';
 import SystemVersion from '~/components/SystemVersion';
 import InterceptorManager from '~/services/interceptor-manager';
-import Xss from '~/services/xss';
 import loggerFactory from '~/utils/logger';
 
 import EmptyTrashModal from '../components/EmptyTrashModal';
 import HotkeysManager from '../components/Hotkeys/HotkeysManager';
-import GrowiNavbar from '../components/Navbar/GrowiNavbar';
-import GrowiNavbarBottom from '../components/Navbar/GrowiNavbarBottom';
+import { GrowiNavbar } from '../components/Navbar/GrowiNavbar';
+import { GrowiNavbarBottom } from '../components/Navbar/GrowiNavbarBottom';
 import PageAccessoriesModal from '../components/PageAccessoriesModal';
 import PageCreateModal from '../components/PageCreateModal';
 import PageDeleteModal from '../components/PageDeleteModal';
@@ -31,17 +29,11 @@ if (!window) {
   window = {};
 }
 
-// setup xss library
-const xss = new Xss();
-window.xss = xss;
-
 window.globalEmitter = new EventEmitter();
 window.interceptorManager = new InterceptorManager();
 
 // create unstated container instance
 const appContainer = new AppContainer();
-// eslint-disable-next-line no-unused-vars
-const socketIoContainer = new SocketIoContainer(appContainer);
 
 appContainer.initApp();
 

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

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

+ 0 - 4
packages/app/src/client/legacy/crowi.js

@@ -1,10 +1,6 @@
-const { blinkElem, blinkSectionHeaderAtBoot } = require('../util/blink-section-header');
-
 /* eslint-disable react/jsx-filename-extension */
 require('jquery.cookie');
 
-require('./thirdparty-js/waves');
-
 const Crowi = {};
 
 if (!window) {

+ 4 - 4
packages/app/src/client/models/MarkdownTable.js

@@ -1,6 +1,6 @@
+import csvToMarkdown from 'csv-to-markdown-table';
 import markdownTable from 'markdown-table';
 import stringWidth from 'string-width';
-import csvToMarkdown from 'csv-to-markdown-table';
 
 // https://github.com/markdown-it/markdown-it/blob/d29f421927e93e88daf75f22089a3e732e195bd2/lib/rules_block/table.js#L83
 // https://regex101.com/r/7BN2fR/7
@@ -8,9 +8,6 @@ const tableAlignmentLineRE = /^[-:|][-:|\s]*$/;
 const tableAlignmentLineNegRE = /^[^-:]*$/; // it is need to check to ignore empty row which is matched above RE
 const linePartOfTableRE = /^\|[^\r\n]*|[^\r\n]*\|$|([^|\r\n]+\|[^|\r\n]*)+/; // own idea
 
-// set up DOMParser
-const domParser = new (window.DOMParser)();
-
 const defaultOptions = { stringLength: stringWidth };
 
 /**
@@ -67,6 +64,9 @@ export default class MarkdownTable {
    * The error message is a innerHTML, so must not assign it into element.innerHTML because it can lead to Mutation-based XSS
    */
   static fromHTMLTableTag(str) {
+    // set up DOMParser
+    const domParser = new (window.DOMParser)();
+
     // use DOMParser to prevent DOM based XSS (https://developer.mozilla.org/en-US/docs/Web/API/DOMParser)
     const dom = domParser.parseFromString(str, 'application/xml');
 

+ 1 - 2
packages/app/src/client/plugin.js

@@ -8,12 +8,11 @@ export default class GrowiPlugin {
    * process plugin entry
    *
    * @param {AppContainer} appContainer
-   * @param {GrowiRenderer} originRenderer The origin instance of GrowiRenderer
    *
    * @memberof CrowiPlugin
    */
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
-  installAll(appContainer, originRenderer) {
+  installAll(appContainer) {
     // import plugin definitions
     let definitions = [];
     try {

+ 1 - 24
packages/app/src/client/services/AdminSocketIoContainer.js

@@ -1,25 +1,2 @@
-import SocketIoContainer from './SocketIoContainer';
-import { toastError } from '../util/apiNotification';
 
-/**
- * A subclass of SocketIoContainer for /admin namespace
- */
-export default class AdminSocketIoContainer extends SocketIoContainer {
-
-  constructor(appContainer) {
-    super(appContainer, '/admin');
-
-    // show toastr
-    this.socket.on('error', (error) => {
-      toastError(new Error(error));
-    });
-  }
-
-  /**
-   * Workaround for the mangling in production build to break constructor.name
-   */
-  static getClassName() {
-    return 'AdminSocketIoContainer';
-  }
-
-}
+export default class AdminSocketIoContainer {}

+ 12 - 35
packages/app/src/client/services/AppContainer.js

@@ -1,7 +1,5 @@
 import { Container } from 'unstated';
 
-
-import GrowiRenderer from '../util/GrowiRenderer';
 import { i18nFactory } from '../util/i18n';
 
 /**
@@ -26,7 +24,6 @@ export default class AppContainer extends Container {
 
     this.containerInstances = {};
     this.componentInstances = {};
-    this.rendererInstances = {};
   }
 
   /**
@@ -45,8 +42,6 @@ export default class AppContainer extends Container {
 
     this.isDocSaved = true;
 
-    this.originRenderer = new GrowiRenderer(this);
-
     const isPluginEnabled = body.dataset.pluginEnabled === 'true';
     if (isPluginEnabled) {
       this.initPlugins();
@@ -57,19 +52,23 @@ export default class AppContainer extends Container {
 
   initPlugins() {
     const growiPlugin = window.growiPlugin;
-    growiPlugin.installAll(this, this.originRenderer);
+    growiPlugin.installAll(this);
   }
 
   injectToWindow() {
-    window.appContainer = this;
+    // for fix lint error
+
+    // window.appContainer = this;
+
+    // const growiRenderer = new GrowiRenderer(this.getConfig());
+    // growiRenderer.init();
 
-    const originRenderer = this.getOriginRenderer();
-    window.growiRenderer = originRenderer;
+    // window.growiRenderer = growiRenderer;
 
-    // backward compatibility
-    window.crowi = this;
-    window.crowiRenderer = originRenderer;
-    window.crowiPlugin = window.growiPlugin;
+    // // backward compatibility
+    // window.crowi = this;
+    // window.crowiRenderer = window.growiRenderer;
+    // window.crowiPlugin = window.growiPlugin;
   }
 
   getConfig() {
@@ -126,26 +125,4 @@ export default class AppContainer extends Container {
     return this.componentInstances[id];
   }
 
-  getOriginRenderer() {
-    return this.originRenderer;
-  }
-
-  /**
-   * factory method
-   */
-  getRenderer(mode) {
-    if (this.rendererInstances[mode] != null) {
-      return this.rendererInstances[mode];
-    }
-
-    const renderer = new GrowiRenderer(this, this.originRenderer);
-    // setup
-    renderer.initMarkdownItConfigurers(mode);
-    renderer.setup(mode);
-    // register
-    this.rendererInstances[mode] = renderer;
-
-    return renderer;
-  }
-
 }

+ 42 - 21
packages/app/src/client/services/ContextExtractor.tsx

@@ -3,7 +3,9 @@ import React, { FC, useEffect, useState } from 'react';
 
 import { pagePathUtils } from '@growi/core';
 
+import { CustomWindow } from '~/interfaces/global';
 import { IUserUISettings } from '~/interfaces/user-ui-settings';
+// import { generatePreviewRenderer } from '~/services/renderer/growi-renderer';
 import {
   useIsDeviceSmallerThanMd, useIsDeviceSmallerThanLg,
   usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed, useCurrentSidebarContents, useCurrentProductNavWidth,
@@ -13,13 +15,14 @@ import { useSetupGlobalSocket, useSetupGlobalAdminSocket } from '~/stores/websoc
 
 import {
   useSiteUrl,
-  useCurrentCreatedAt, useDeleteUsername, useDeletedAt, useHasChildren, useHasDraftOnHackmd,
+  useDeleteUsername, useDeletedAt, useHasChildren, useHasDraftOnHackmd,
   useIsNotCreatable, useIsTrashPage, useIsUserPage, useLastUpdateUsername,
   useCurrentPageId, usePageIdOnHackmd, usePageUser, useCurrentPagePath, useRevisionCreatedAt, useRevisionId, useRevisionIdHackmdSynced,
-  useShareLinkId, useShareLinksNumber, useTemplateTagData, useCurrentUpdatedAt, useCreator, useRevisionAuthor, useCurrentUser, useTargetAndAncestors,
-  useNotFoundTargetPathOrId, useIsSearchPage, useIsForbidden, useIsIdenticalPath, useHasParent,
+  useShareLinkId, useShareLinksNumber, useTemplateTagData, useCurrentUser, useTargetAndAncestors,
+  useIsSearchPage, useIsForbidden, useIsIdenticalPath, useHasParent,
   useIsAclEnabled, useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsEnabledAttachTitleHeader,
-  useDefaultIndentSize, useIsIndentSizeForced, useCsrfToken, useIsEmptyPage, useEmptyPageId, useGrowiVersion,
+  useDefaultIndentSize, useIsIndentSizeForced, useCsrfToken, useGrowiVersion, useAuditLogEnabled,
+  useActivityExpirationSeconds, useAuditLogAvailableActions, useRendererConfig,
 } from '../../stores/context';
 
 const { isTrashPage: _isTrashPage } = pagePathUtils;
@@ -60,17 +63,9 @@ const ContextExtractorOnce: FC = () => {
   const path = decodeURI(mainContent?.getAttribute('data-path') || '');
   // assign `null` to avoid returning empty string
   const pageId = mainContent?.getAttribute('data-page-id') || null;
-  const emptyPageId = notFoundContext?.getAttribute('data-page-id') || null;
 
   const revisionCreatedAt = +(mainContent?.getAttribute('data-page-revision-created') || '');
 
-  // createdAt
-  const createdAtAttribute = mainContent?.getAttribute('data-page-created-at');
-  const createdAt: Date | null = (createdAtAttribute != null) ? new Date(createdAtAttribute) : null;
-  // updatedAt
-  const updatedAtAttribute = mainContent?.getAttribute('data-page-updated-at');
-  const updatedAt: Date | null = (updatedAtAttribute != null) ? new Date(updatedAtAttribute) : null;
-
   const deletedAt = mainContent?.getAttribute('data-page-deleted-at') || null;
   const isIdenticalPath = JSON.parse(mainContent?.getAttribute('data-identical-path') || jsonNull) ?? false;
   const isUserPage = JSON.parse(mainContent?.getAttribute('data-page-user') || jsonNull) != null;
@@ -88,8 +83,6 @@ const ContextExtractorOnce: FC = () => {
   const deleteUsername = mainContent?.getAttribute('data-page-delete-username') || null;
   const pageIdOnHackmd = mainContent?.getAttribute('data-page-id-on-hackmd') || null;
   const hasDraftOnHackmd = !!mainContent?.getAttribute('data-page-has-draft-on-hackmd');
-  const creator = JSON.parse(mainContent?.getAttribute('data-page-creator') || jsonNull);
-  const revisionAuthor = JSON.parse(mainContent?.getAttribute('data-page-revision-author') || jsonNull);
   const targetAndAncestors = JSON.parse(document.getElementById('growi-pagetree-target-and-ancestors')?.textContent || jsonNull);
   const notFoundTargetPathOrId = JSON.parse(notFoundContentForPt?.getAttribute('data-not-found-target-path-or-id') || jsonNull);
   const isSearchPage = document.getElementById('search-page') != null;
@@ -122,10 +115,26 @@ const ContextExtractorOnce: FC = () => {
   useIsEnabledAttachTitleHeader(configByContextHydrate.isEnabledAttachTitleHeader);
   useIsIndentSizeForced(configByContextHydrate.isIndentSizeForced);
   useDefaultIndentSize(configByContextHydrate.adminPreferredIndentSize);
+  useAuditLogEnabled(configByContextHydrate.auditLogEnabled);
+  useActivityExpirationSeconds(configByContextHydrate.activityExpirationSeconds);
+  useAuditLogAvailableActions(configByContextHydrate.auditLogAvailableActions);
   useGrowiVersion(configByContextHydrate.crowi.version);
+  useRendererConfig({
+    isEnabledLinebreaks: configByContextHydrate.isEnabledLinebreaks,
+    isEnabledLinebreaksInComments: configByContextHydrate.isEnabledLinebreaksInComments,
+    adminPreferredIndentSize: configByContextHydrate.adminPreferredIndentSize,
+    isIndentSizeForced: configByContextHydrate.isIndentSizeForced,
+
+    isEnabledXssPrevention: configByContextHydrate.isEnabledXssPrevention,
+    attrWhiteList: configByContextHydrate.attrWhiteList,
+    tagWhiteList: configByContextHydrate.tagWhiteList,
+    highlightJsStyleBorder: configByContextHydrate.highlightJsStyleBorder,
+
+    plantumlUri: configByContextHydrate.env.PLANTUML_URI,
+    blockdiagUri: configByContextHydrate.env.BLOCKDIAG_URI,
+  });
 
   // Page
-  useCurrentCreatedAt(createdAt);
   useDeleteUsername(deleteUsername);
   useDeletedAt(deletedAt);
   useHasChildren(hasChildren);
@@ -137,7 +146,6 @@ const ContextExtractorOnce: FC = () => {
   useIsUserPage(isUserPage);
   useLastUpdateUsername(lastUpdateUsername);
   useCurrentPageId(pageId);
-  useEmptyPageId(emptyPageId);
   usePageIdOnHackmd(pageIdOnHackmd);
   usePageUser(pageUser);
   useCurrentPagePath(path);
@@ -147,13 +155,8 @@ const ContextExtractorOnce: FC = () => {
   useShareLinkId(shareLinkId);
   useShareLinksNumber(shareLinksNumber);
   useTemplateTagData(templateTagData);
-  useCurrentUpdatedAt(updatedAt);
-  useCreator(creator);
-  useRevisionAuthor(revisionAuthor);
   useTargetAndAncestors(targetAndAncestors);
-  useNotFoundTargetPathOrId(notFoundTargetPathOrId);
   useIsSearchPage(isSearchPage);
-  useIsEmptyPage(isEmptyPage);
   useHasParent(hasParent);
 
   // Navigation
@@ -179,6 +182,24 @@ const ContextExtractorOnce: FC = () => {
   const shouldInitAdminSock = !!currentUser?.isAdmin;
   useSetupGlobalAdminSocket(shouldInitAdminSock);
 
+  // TODO: Remove this code when reveal.js is omitted. see: https://github.com/weseek/growi/pull/6223
+  // Do not access this property from other than reveal.js plugins.
+  // (window as CustomWindow).previewRenderer = generatePreviewRenderer(
+  //   {
+  //     isEnabledXssPrevention: configByContextHydrate.isEnabledXssPrevention,
+  //     attrWhiteList: configByContextHydrate.attrWhiteList,
+  //     tagWhiteList: configByContextHydrate.tagWhiteList,
+  //     highlightJsStyleBorder: configByContextHydrate.highlightJsStyleBorder,
+  //     env: {
+  //       MATHJAX: configByContextHydrate.env.MATHJAX,
+  //       PLANTUML_URI: configByContextHydrate.env.PLANTUML_URI,
+  //       BLOCKDIAG_URI: configByContextHydrate.env.BLOCKDIAG_URI,
+  //     },
+  //   },
+  //   null,
+  //   path,
+  // );
+
   return null;
 };
 

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

@@ -13,10 +13,10 @@ import { apiv3Post } from '../util/apiv3-client';
 import {
   DetachCodeBlockInterceptor,
   RestoreCodeBlockInterceptor,
-} from '../util/interceptor/detach-code-blocks';
+} from '../../services/renderer/interceptor/detach-code-blocks';
 import {
   DrawioInterceptor,
-} from '../util/interceptor/drawio-interceptor';
+} from '../../services/renderer/interceptor/drawio-interceptor';
 
 const { isTrashPage } = pagePathUtils;
 
@@ -54,9 +54,6 @@ export default class PageContainer extends Container {
       path,
       isEmpty: mainContent.getAttribute('data-page-is-empty'),
 
-      createdAt: mainContent.getAttribute('data-page-created-at'),
-      // please use useCurrentUpdatedAt instead
-      updatedAt: mainContent.getAttribute('data-page-updated-at'),
       deletedAt: mainContent.getAttribute('data-page-deleted-at') || null,
 
       isUserPage: JSON.parse(mainContent.getAttribute('data-page-user')) != null,

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

@@ -1,186 +0,0 @@
-import { Container } from 'unstated';
-
-import loggerFactory from '~/utils/logger';
-
-import { apiPost } from '../util/apiv1-client';
-import { apiv3Get, apiv3Put } from '../util/apiv3-client';
-
-// eslint-disable-next-line no-unused-vars
-const logger = loggerFactory('growi:services:PersonalContainer');
-
-/**
- * Service container for personal settings page (PersonalSettings.jsx)
- * @extends {Container} unstated Container
- */
-export default class PersonalContainer extends Container {
-
-  constructor(appContainer) {
-    super();
-
-    this.appContainer = appContainer;
-
-    this.state = {
-      retrieveError: null,
-      name: '',
-      email: '',
-      registrationWhiteList: this.appContainer.getConfig().registrationWhiteList,
-      isEmailPublished: false,
-      lang: 'en_US',
-      isGravatarEnabled: false,
-      externalAccounts: [],
-      apiToken: '',
-      slackMemberId: '',
-    };
-
-  }
-
-  /**
-   * Workaround for the mangling in production build to break constructor.name
-   */
-  static getClassName() {
-    return 'PersonalContainer';
-  }
-
-  /**
-   * retrieve personal data
-   */
-  async retrievePersonalData() {
-    try {
-      const response = await apiv3Get('/personal-setting/');
-      const { currentUser } = response.data;
-      this.setState({
-        name: currentUser.name,
-        email: currentUser.email,
-        isEmailPublished: currentUser.isEmailPublished,
-        lang: currentUser.lang,
-        isGravatarEnabled: currentUser.isGravatarEnabled,
-        apiToken: currentUser.apiToken,
-        slackMemberId: currentUser.slackMemberId,
-      });
-    }
-    catch (err) {
-      this.setState({ retrieveError: err });
-      logger.error(err);
-      throw new Error('Failed to fetch personal data');
-    }
-  }
-
-  /**
-   * retrieve external accounts that linked me
-   */
-  async retrieveExternalAccounts() {
-    try {
-      const response = await apiv3Get('/personal-setting/external-accounts');
-      const { externalAccounts } = response.data;
-
-      this.setState({ externalAccounts });
-    }
-    catch (err) {
-      this.setState({ retrieveError: err });
-      logger.error(err);
-      throw new Error('Failed to fetch external accounts');
-    }
-  }
-
-  /**
-   * Change name
-   */
-  changeName(inputValue) {
-    this.setState({ name: inputValue });
-  }
-
-  /**
-   * Change email
-   */
-  changeEmail(inputValue) {
-    this.setState({ email: inputValue });
-  }
-
-  /**
-   * Change Slack Member ID
-   */
-  changeSlackMemberId(inputValue) {
-    this.setState({ slackMemberId: inputValue });
-  }
-
-  /**
-   * Change isEmailPublished
-   */
-  changeIsEmailPublished(boolean) {
-    this.setState({ isEmailPublished: boolean });
-  }
-
-  /**
-   * Change lang
-   */
-  changeLang(lang) {
-    this.setState({ lang });
-  }
-
-  /**
-   * Change isGravatarEnabled
-   */
-  changeIsGravatarEnabled(boolean) {
-    this.setState({ isGravatarEnabled: boolean });
-  }
-
-  /**
-   * Update basic info
-   * @memberOf PersonalContainer
-   * @return {Array} basic info
-   */
-  async updateBasicInfo() {
-    try {
-      const response = await apiv3Put('/personal-setting/', {
-        name: this.state.name,
-        email: this.state.email,
-        isEmailPublished: this.state.isEmailPublished,
-        lang: this.state.lang,
-        slackMemberId: this.state.slackMemberId,
-      });
-      const { updatedUser } = response.data;
-
-      this.setState({
-        name: updatedUser.name,
-        email: updatedUser.email,
-        isEmailPublished: updatedUser.isEmailPublished,
-        lang: updatedUser.lang,
-        slackMemberId: updatedUser.slackMemberId,
-      });
-    }
-    catch (err) {
-      this.setState({ retrieveError: err });
-      logger.error(err);
-      throw new Error('Failed to update personal data');
-    }
-  }
-
-  /**
-   * Associate LDAP account
-   */
-  async associateLdapAccount(account) {
-    try {
-      await apiv3Put('/personal-setting/associate-ldap', account);
-    }
-    catch (err) {
-      this.setState({ retrieveError: err });
-      logger.error(err);
-      throw new Error('Failed to associate ldap account');
-    }
-  }
-
-  /**
-   * Disassociate LDAP account
-   */
-  async disassociateLdapAccount(account) {
-    try {
-      await apiv3Put('/personal-setting/disassociate-ldap', account);
-    }
-    catch (err) {
-      this.setState({ retrieveError: err });
-      logger.error(err);
-      throw new Error('Failed to disassociate ldap account');
-    }
-  }
-
-}

+ 1 - 2
packages/app/src/client/services/page-operation.ts

@@ -1,7 +1,6 @@
+import { SubscriptionStatusType } from '@growi/core';
 import urljoin from 'url-join';
 
-import { SubscriptionStatusType } from '~/interfaces/subscription';
-
 import { toastError } from '../util/apiNotification';
 import { apiv3Post, apiv3Put } from '../util/apiv3-client';
 

+ 0 - 208
packages/app/src/client/util/GrowiRenderer.js

@@ -1,208 +0,0 @@
-import MarkdownIt from 'markdown-it';
-
-import loggerFactory from '~/utils/logger';
-
-import CsvToTable from './PreProcessor/CsvToTable';
-import EasyGrid from './PreProcessor/EasyGrid';
-import Linker from './PreProcessor/Linker';
-import XssFilter from './PreProcessor/XssFilter';
-import BlockdiagConfigurer from './markdown-it/blockdiag';
-import DrawioViewerConfigurer from './markdown-it/drawio-viewer';
-import EmojiConfigurer from './markdown-it/emoji';
-import FooternoteConfigurer from './markdown-it/footernote';
-import HeaderConfigurer from './markdown-it/header';
-import HeaderLineNumberConfigurer from './markdown-it/header-line-number';
-import HeaderWithEditLinkConfigurer from './markdown-it/header-with-edit-link';
-import LinkerByRelativePathConfigurer from './markdown-it/link-by-relative-path';
-import MathJaxConfigurer from './markdown-it/mathjax';
-import PlantUMLConfigurer from './markdown-it/plantuml';
-import TableConfigurer from './markdown-it/table';
-import TableWithHandsontableButtonConfigurer from './markdown-it/table-with-handsontable-button';
-import TaskListsConfigurer from './markdown-it/task-lists';
-import TocAndAnchorConfigurer from './markdown-it/toc-and-anchor';
-
-const logger = loggerFactory('growi:util:GrowiRenderer');
-
-export default class GrowiRenderer {
-
-  /**
-   *
-   * @param {AppContainer} appContainer
-   * @param {GrowiRenderer} originRenderer
-   * @param {string} mode
-   */
-  constructor(appContainer, originRenderer) {
-    this.appContainer = appContainer;
-
-    if (originRenderer != null) {
-      this.preProcessors = originRenderer.preProcessors;
-      this.postProcessors = originRenderer.postProcessors;
-    }
-    else {
-      this.preProcessors = [
-        new EasyGrid(),
-        new Linker(),
-        new CsvToTable(),
-        new XssFilter(appContainer),
-      ];
-      this.postProcessors = [
-      ];
-    }
-
-    this.initMarkdownItConfigurers = this.initMarkdownItConfigurers.bind(this);
-    this.setup = this.setup.bind(this);
-    this.process = this.process.bind(this);
-    this.codeRenderer = this.codeRenderer.bind(this);
-  }
-
-  initMarkdownItConfigurers(mode) {
-    const appContainer = this.appContainer;
-
-    // init markdown-it
-    this.md = new MarkdownIt({
-      html: true,
-      linkify: true,
-      highlight: this.codeRenderer,
-    });
-
-    this.isMarkdownItConfigured = false;
-
-    this.markdownItConfigurers = [
-      new LinkerByRelativePathConfigurer(appContainer),
-      new TaskListsConfigurer(appContainer),
-      new HeaderConfigurer(),
-      new EmojiConfigurer(),
-      new MathJaxConfigurer(appContainer),
-      new DrawioViewerConfigurer(),
-      new PlantUMLConfigurer(appContainer),
-      new BlockdiagConfigurer(appContainer),
-    ];
-
-    // add configurers according to mode
-    switch (mode) {
-      case 'page': {
-        this.markdownItConfigurers = this.markdownItConfigurers.concat([
-          new FooternoteConfigurer(),
-          new TocAndAnchorConfigurer(),
-          new HeaderLineNumberConfigurer(),
-          new HeaderWithEditLinkConfigurer(),
-          new TableWithHandsontableButtonConfigurer(),
-        ]);
-        break;
-      }
-      case 'editor':
-        this.markdownItConfigurers = this.markdownItConfigurers.concat([
-          new FooternoteConfigurer(),
-          new HeaderLineNumberConfigurer(),
-          new TableConfigurer(),
-        ]);
-        break;
-      // case 'comment':
-      //   break;
-      default:
-        this.markdownItConfigurers = this.markdownItConfigurers.concat([
-          new TableConfigurer(),
-        ]);
-        break;
-    }
-  }
-
-  /**
-   * setup with crowi config
-   */
-  setup(mode) {
-    const crowiConfig = this.appContainer.config;
-
-    let isEnabledLinebreaks;
-    switch (mode) {
-      case 'comment':
-        isEnabledLinebreaks = crowiConfig.isEnabledLinebreaksInComments;
-        break;
-      default:
-        isEnabledLinebreaks = crowiConfig.isEnabledLinebreaks;
-        break;
-    }
-
-    this.md.set({
-      breaks: isEnabledLinebreaks,
-    });
-
-    if (!this.isMarkdownItConfigured) {
-      this.markdownItConfigurers.forEach((configurer) => {
-        configurer.configure(this.md);
-      });
-    }
-  }
-
-  preProcess(markdown, context) {
-    let processed = markdown;
-    for (let i = 0; i < this.preProcessors.length; i++) {
-      if (!this.preProcessors[i].process) {
-        continue;
-      }
-      processed = this.preProcessors[i].process(processed, context);
-    }
-
-    return processed;
-  }
-
-  process(markdown, context) {
-    return this.md.render(markdown, context);
-  }
-
-  postProcess(html, context) {
-    let processed = html;
-    for (let i = 0; i < this.postProcessors.length; i++) {
-      if (!this.postProcessors[i].process) {
-        continue;
-      }
-      processed = this.postProcessors[i].process(processed, context);
-    }
-
-    return processed;
-  }
-
-  codeRenderer(code, langExt) {
-    const config = this.appContainer.getConfig();
-    const noborder = (!config.highlightJsStyleBorder) ? 'hljs-no-border' : '';
-
-    let citeTag = '';
-    let hljsLang = 'plaintext';
-    let showLinenumbers = false;
-
-    if (langExt) {
-      // https://regex101.com/r/qGs7eZ/3
-      const match = langExt.match(/^([^:=\n]+)?(=([^:=\n]*))?(:([^:=\n]*))?(=([^:=\n]*))?$/);
-
-      const lang = match[1];
-      const fileName = match[5] || null;
-      showLinenumbers = (match[2] != null) || (match[6] != null);
-
-      if (fileName != null) {
-        citeTag = `<cite>${fileName}</cite>`;
-      }
-      if (hljs.getLanguage(lang)) {
-        hljsLang = lang;
-      }
-    }
-
-    let highlightCode = code;
-    try {
-      highlightCode = hljs.highlight(hljsLang, code, true).value;
-
-      // add line numbers
-      if (showLinenumbers) {
-        highlightCode = hljs.lineNumbersValue((highlightCode));
-      }
-    }
-    catch (err) {
-      logger.error(err);
-    }
-
-    return `<pre class="hljs ${noborder}">${citeTag}<code>${highlightCode}</code></pre>`;
-  }
-
-  highlightCode(code, lang) {
-  }
-
-}

+ 0 - 23
packages/app/src/client/util/PreProcessor/XssFilter.js

@@ -1,23 +0,0 @@
-import Xss from '~/services/xss';
-import XssOption from '~/services/xss/xssOption';
-
-export default class XssFilter {
-
-  constructor(crowi) {
-    this.crowi = crowi;
-
-    if (crowi.config.isEnabledXssPrevention) {
-      this.xssOption = new XssOption(crowi.config);
-      this.xss = new Xss(this.xssOption);
-    }
-  }
-
-  process(markdown) {
-    if (this.crowi.config.isEnabledXssPrevention) {
-      return this.xss.process(markdown);
-    }
-
-    return markdown;
-  }
-
-}

+ 1 - 0
packages/app/src/client/util/blink-section-header.ts

@@ -23,5 +23,6 @@ export const blinkSectionHeaderAtBoot = (): HTMLElement | undefined => {
   const elem = document.getElementById(id);
   if (elem != null && elem.tagName.match(/h\d+/i)) { // match h1, h2, h3...
     blinkElem(elem);
+    return elem;
   }
 };

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

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

+ 4 - 0
packages/app/src/client/util/i18n.js

@@ -1,3 +1,4 @@
+
 /* eslint-disable */
 import i18n from 'i18next';
 import LanguageDetector from 'i18next-browser-languagedetector';
@@ -15,6 +16,9 @@ Object.values(locales).forEach((locale) => {
   });
 });
 
+/*
+* Note: This file will be deleted. use "~/next-i18next.config" instead
+*/
 // extract metadata list from 'public/static/locales/${locale}/meta.json'
 export const localeMetadatas = Object.values(locales).map(locale => locale.meta);
 

+ 0 - 17
packages/app/src/client/util/markdown-it/blockdiag.js

@@ -1,17 +0,0 @@
-export default class BlockdiagConfigurer {
-
-  constructor(crowi) {
-    this.crowi = crowi;
-    const config = crowi.getConfig();
-
-    this.generateSourceUrl = config.env.BLOCKDIAG_URI || 'https://blockdiag-api.com/';
-  }
-
-  configure(md) {
-    md.use(require('markdown-it-blockdiag'), {
-      generateSourceUrl: this.generateSourceUrl,
-      marker: ':::',
-    });
-  }
-
-}

+ 0 - 9
packages/app/src/client/util/markdown-it/drawio-viewer.js

@@ -1,9 +0,0 @@
-export default class DrawioViewerConfigurer {
-
-  configure(md) {
-    md.use(require('markdown-it-drawio-viewer'), {
-      marker: ':::',
-    });
-  }
-
-}

+ 0 - 12
packages/app/src/client/util/markdown-it/emoji.js

@@ -1,12 +0,0 @@
-import markdownItEmojiMart from 'markdown-it-emoji-mart';
-
-import { emojiMartData } from './emoji-mart-data';
-
-
-export default class EmojiConfigurer {
-
-  configure(md) {
-    md.use(markdownItEmojiMart, { defs: emojiMartData });
-  }
-
-}

+ 0 - 16
packages/app/src/client/util/markdown-it/mathjax.js

@@ -1,16 +0,0 @@
-export default class MathJaxConfigurer {
-
-  constructor(crowi) {
-    this.crowi = crowi;
-
-    const config = crowi.getConfig();
-    this.isEnabled = !!config.env.MATHJAX; // convert to boolean
-  }
-
-  configure(md) {
-    if (this.isEnabled) {
-      md.use(require('markdown-it-mathjax')());
-    }
-  }
-
-}

+ 0 - 18
packages/app/src/client/util/markdown-it/task-lists.js

@@ -1,18 +0,0 @@
-export default class TaskListsConfigurer {
-
-  constructor(crowi) {
-    this.crowi = crowi;
-  }
-
-  configure(md) {
-    md.use(require('markdown-it-task-checkbox'), {
-      disabled: true,
-      divWrap: true,
-      divClass: 'checkbox checkbox-primary',
-      idPrefix: 'cbx_',
-      ulClass: 'task-list',
-      liClass: 'task-list-item',
-    });
-  }
-
-}

+ 0 - 27
packages/app/src/client/util/markdown-it/toc-and-anchor.js

@@ -1,27 +0,0 @@
-import markdownItEmojiMart from 'markdown-it-emoji-mart';
-import markdownItToc from 'markdown-it-toc-and-anchor-with-slugid';
-
-import { emojiMartData } from './emoji-mart-data';
-
-export default class TocAndAnchorConfigurer {
-
-  configure(md) {
-    md.use(markdownItEmojiMart, { defs: emojiMartData })
-      .use(markdownItToc, {
-        tocLastLevel: 3,
-        anchorLinkBefore: false,
-        anchorLinkSymbol: '',
-        anchorLinkSymbolClassName: 'icon-link',
-        anchorClassName: 'revision-head-link',
-      });
-
-    // set toc render function
-    md.set({
-      tocCallback: (tocMarkdown, tocArray, tocHtml) => {
-        // eslint-disable-next-line no-undef
-        globalEmitter.emit('renderTocHtml', tocHtml);
-      },
-    });
-  }
-
-}

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

@@ -2,19 +2,16 @@
  * reveal.js growi-renderer plugin.
  */
 (function(root, factory) {
-  // get AppContainer instance from parent window
-  const appContainer = window.parent.appContainer;
-
-  const growiRendererPlugin = factory(appContainer);
+  const growiRendererPlugin = factory();
   growiRendererPlugin.initialize();
-}(this, (appContainer) => {
+}(this, () => {
   /* eslint-disable no-useless-escape */
   const DEFAULT_SLIDE_SEPARATOR = '^\r?\n---\r?\n$';
   const DEFAULT_ELEMENT_ATTRIBUTES_SEPARATOR = '\\\.element\\\s*?(.+?)$';
   const DEFAULT_SLIDE_ATTRIBUTES_SEPARATOR = '\\\.slide:\\\s*?(\\\S.+?)$';
   /* eslint-enable no-useless-escape */
 
-  const growiRenderer = appContainer.getRenderer('editor');
+  const growiRenderer = window.parent.previewRenderer;
 
   let marked;
 
@@ -61,7 +58,7 @@
         section.setAttribute('data-markdown-parsed', 'true');
         const notes = section.querySelector('aside.notes');
         markdown = marked.getMarkdownFromSlide(section);
-        const context = { markdown };
+        const context = { markdown, currentPathname: decodeURIComponent(window.parent.location.pathname) };
 
         interceptorManager.process('preRender', context)
           .then(() => { return interceptorManager.process('prePreProcess', context) })

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

@@ -1,19 +1,22 @@
 import React, { useEffect, useCallback } from 'react';
-import PropTypes from 'prop-types';
+
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
 import { Tooltip } from 'reactstrap';
-import loggerFactory from '~/utils/logger';
 
+import AdminHomeContainer from '~/client/services/AdminHomeContainer';
 import { toastError } from '~/client/util/apiNotification';
+import { useSWRxV5MigrationStatus } from '~/stores/page-listing';
+import loggerFactory from '~/utils/logger';
+
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
-import AppContainer from '~/client/services/AppContainer';
-import AdminHomeContainer from '~/client/services/AdminHomeContainer';
-import { useSWRxV5MigrationStatus } from '~/stores/page-listing';
-import SystemInfomationTable from './SystemInfomationTable';
-import InstalledPluginTable from './InstalledPluginTable';
+
+
 import EnvVarsTable from './EnvVarsTable';
+import InstalledPluginTable from './InstalledPluginTable';
+import SystemInfomationTable from './SystemInfomationTable';
 
 const logger = loggerFactory('growi:admin');
 
@@ -129,10 +132,9 @@ const AdminHome = (props) => {
 };
 
 
-const AdminHomeWrapper = withUnstatedContainers(AdminHome, [AppContainer, AdminHomeContainer]);
+const AdminHomeWrapper = withUnstatedContainers(AdminHome, [AdminHomeContainer]);
 
 AdminHome.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminHomeContainer: PropTypes.instanceOf(AdminHomeContainer).isRequired,
 };
 

+ 23 - 18
packages/app/src/components/Admin/App/AppSetting.jsx

@@ -1,11 +1,11 @@
 import React, { useCallback } from 'react';
 
+import { useTranslation, i18n } from 'next-i18next';
 import PropTypes from 'prop-types';
-import { useTranslation } from 'next-i18next';
 
 import AdminAppContainer from '~/client/services/AdminAppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
-import { localeMetadatas } from '~/client/util/i18n';
+import { i18n as i18nConfig } from '~/next-i18next.config';
 import loggerFactory from '~/utils/logger';
 
 
@@ -77,22 +77,27 @@ const AppSetting = (props) => {
         </label>
         <div className="col-md-6 py-2">
           {
-            localeMetadatas.map(meta => (
-              <div key={meta.id} className="custom-control custom-radio custom-control-inline">
-                <input
-                  type="radio"
-                  id={`radioLang${meta.id}`}
-                  className="custom-control-input"
-                  name="globalLang"
-                  value={meta.id}
-                  checked={adminAppContainer.state.globalLang === meta.id}
-                  onChange={(e) => {
-                    adminAppContainer.changeGlobalLang(e.target.value);
-                  }}
-                />
-                <label className="custom-control-label" htmlFor={`radioLang${meta.id}`}>{meta.displayName}</label>
-              </div>
-            ))
+            i18nConfig.locales.map((locale) => {
+              const fixedT = i18n.getFixedT(locale);
+              i18n.loadLanguages(i18nConfig.locales);
+
+              return (
+                <div key={locale} className="custom-control custom-radio custom-control-inline">
+                  <input
+                    type="radio"
+                    id={`radioLang${locale}`}
+                    className="custom-control-input"
+                    name="globalLang"
+                    value={locale}
+                    checked={adminAppContainer.state.globalLang === locale}
+                    onChange={(e) => {
+                      adminAppContainer.changeGlobalLang(e.target.value);
+                    }}
+                  />
+                  <label className="custom-control-label" htmlFor={`radioLang${locale}`}>{fixedT('meta.display_name')}</label>
+                </div>
+              );
+            })
           }
         </div>
       </div>

+ 47 - 0
packages/app/src/components/Admin/AuditLog/ActivityTable.tsx

@@ -0,0 +1,47 @@
+import React, { FC } from 'react';
+
+import { format } from 'date-fns';
+import { useTranslation } from 'react-i18next';
+
+import { IActivityHasId } from '~/interfaces/activity';
+
+type Props = {
+  activityList: IActivityHasId[]
+}
+
+const formatDate = (date) => {
+  return format(new Date(date), 'yyyy/MM/dd HH:mm:ss');
+};
+
+export const ActivityTable : FC<Props> = (props: Props) => {
+  const { t } = useTranslation();
+
+  return (
+    <div className="table-responsive text-nowrap h-100">
+      <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.date')}</th>
+            <th scope="col">{t('admin:audit_log_management.action')}</th>
+            <th scope="col">{t('admin:audit_log_management.ip')}</th>
+            <th scope="col">{t('admin:audit_log_management.url')}</th>
+          </tr>
+        </thead>
+        <tbody>
+          {props.activityList.map((activity) => {
+            return (
+              <tr data-testid="activity-table" key={activity._id}>
+                <td>{activity.snapshot?.username}</td>
+                <td>{formatDate(activity.createdAt)}</td>
+                <td>{t(`admin:audit_log_action.${activity.action}`)}</td>
+                <td>{activity.ip}</td>
+                <td>{activity.endpoint}</td>
+              </tr>
+            );
+          })}
+        </tbody>
+      </table>
+    </div>
+  );
+};

+ 26 - 0
packages/app/src/components/Admin/AuditLog/AuditLogDisableMode.tsx

@@ -0,0 +1,26 @@
+import React, { FC } from 'react';
+
+import { useTranslation } from 'react-i18next';
+
+export const AuditLogDisableMode: FC = () => {
+  const { t } = useTranslation();
+
+  return (
+    <div id="content-main" className="content-main container-lg">
+      <div className="container">
+        <div className="row justify-content-md-center">
+          <div className="col-md-6 mt-5">
+            <div className="text-center">
+              <h1><i className="icon-exclamation large"></i></h1>
+              <h1 className="text-center">{t('AuditLog')}</h1>
+              <h3
+                // eslint-disable-next-line react/no-danger
+                dangerouslySetInnerHTML={{ __html: t('admin:audit_log_management.disable_mode_explain') }}
+              />
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+};

+ 54 - 0
packages/app/src/components/Admin/AuditLog/AuditLogSettings.tsx

@@ -0,0 +1,54 @@
+import React, { FC, useState } from 'react';
+
+import { useTranslation } from 'react-i18next';
+import { Collapse } from 'reactstrap';
+
+import { useActivityExpirationSeconds, useAuditLogAvailableActions } from '~/stores/context';
+
+export const AuditLogSettings: FC = () => {
+  const { t } = useTranslation();
+
+  const [isExpandActionList, setIsExpandActionList] = useState(false);
+
+  const { data: activityExpirationSecondsData } = useActivityExpirationSeconds();
+  const activityExpirationSeconds = activityExpirationSecondsData != null ? activityExpirationSecondsData : 2592000;
+
+  const { data: availableActionsData } = useAuditLogAvailableActions();
+  const availableActions = availableActionsData != null ? availableActionsData : [];
+
+  return (
+    <>
+      <h4 className="mt-4">{t('admin:audit_log_management.activity_expiration_date')}</h4>
+      <p className="form-text text-muted">
+        {t('admin:audit_log_management.activity_expiration_date_explain')}
+      </p>
+      <p className="alert alert-warning col-6">
+        <i className="icon-exclamation icon-fw">
+        </i><b>FIXED</b><br />
+        <b
+          // eslint-disable-next-line react/no-danger
+          dangerouslySetInnerHTML={{
+            __html: t('admin:audit_log_management.fixed_by_env_var',
+              { key: 'ACTIVITY_EXPIRATION_SECONDS', value: activityExpirationSeconds }),
+          }}
+        />
+      </p>
+
+      <h4 className="mt-4">{t('admin:audit_log_management.available_action_list')}</h4>
+      <p className="form-text text-muted">{t('admin:audit_log_management.available_action_list_explain')}</p>
+      <p className="mt-1">
+        <button type="button" className="btn btn-link p-0" aria-expanded="false" onClick={() => setIsExpandActionList(!isExpandActionList)}>
+          <i className={`fa fa-fw fa-arrow-right ${isExpandActionList ? 'fa-rotate-90' : ''}`}></i>
+          { t('admin:audit_log_management.action_list') }
+        </button>
+      </p>
+      <Collapse isOpen={isExpandActionList}>
+        <ul className="list-group">
+          { availableActions.map(action => (
+            <li key={action} className="list-group-item">{t(`admin:audit_log_action.${action}`)}</li>
+          )) }
+        </ul>
+      </Collapse>
+    </>
+  );
+};

+ 67 - 0
packages/app/src/components/Admin/AuditLog/DateRangePicker.tsx

@@ -0,0 +1,67 @@
+import React, {
+  FC, useRef, forwardRef, useCallback,
+} from 'react';
+
+import DatePicker from 'react-datepicker';
+import 'react-datepicker/dist/react-datepicker.css';
+
+import { useTranslation } from 'react-i18next';
+
+
+type CustomInputProps = {
+  buttonRef: React.Ref<HTMLButtonElement>
+  onClick?: () => void;
+}
+
+const CustomInput = forwardRef<HTMLButtonElement, CustomInputProps>((props: CustomInputProps) => {
+  const { t } = useTranslation();
+  return (
+    <button
+      type="button"
+      className="btn btn-outline-secondary dropdown-toggle"
+      ref={props.buttonRef}
+      onClick={props.onClick}
+    >
+      <i className="fa fa-fw fa-calendar" /> {t('admin:audit_log_management.date')}
+    </button>
+  );
+});
+
+CustomInput.displayName = 'CustomInput';
+
+type DateRangePickerProps = {
+  startDate: Date | null
+  endDate: Date | null
+  onChange: (dateList: Date[] | null[]) => void
+}
+
+export const DateRangePicker: FC<DateRangePickerProps> = (props: DateRangePickerProps) => {
+  const { startDate, endDate, onChange } = props;
+
+  const buttonRef = useRef(null);
+
+  const changeHandler = useCallback((dateList: Date[] | null[]) => {
+    if (onChange != null) {
+      const [start, end] = dateList;
+      const isSameTime = (start != null && end != null) && (start.getTime() === end.getTime());
+      if (isSameTime) {
+        onChange([null, null]);
+      }
+      else {
+        onChange(dateList);
+      }
+    }
+  }, [onChange]);
+
+  return (
+    <div className="btn-group mr-2">
+      <DatePicker
+        selectsRange
+        startDate={startDate}
+        endDate={endDate}
+        onChange={changeHandler}
+        customInput={<CustomInput buttonRef={buttonRef} />}
+      />
+    </div>
+  );
+};

+ 122 - 0
packages/app/src/components/Admin/AuditLog/SearchUsernameTypeahead.tsx

@@ -0,0 +1,122 @@
+import React, {
+  FC, Fragment, useState, useCallback,
+} from 'react';
+
+import { AsyncTypeahead, Menu, MenuItem } from 'react-bootstrap-typeahead';
+import { useTranslation } from 'react-i18next';
+
+import { useSWRxUsernames } from '~/stores/user';
+
+
+const Categories = {
+  activeUser: 'Active User',
+  inactiveUser: 'Inactive User',
+  activitySnapshotUser: 'Activity Snapshot User',
+} as const;
+
+type CategoryType = typeof Categories[keyof typeof Categories]
+
+type UserDataType = {
+  username: string
+  category: CategoryType
+}
+
+type Props = {
+  onChange: (text: string[]) => void
+}
+
+export const SearchUsernameTypeahead: FC<Props> = (props: Props) => {
+  const { onChange } = props;
+  const { t } = useTranslation();
+
+  /*
+   * State
+   */
+  const [searchKeyword, setSearchKeyword] = useState<string>('');
+
+  /*
+   * Fetch
+   */
+  const requestOptions = { isIncludeActiveUser: true, isIncludeInactiveUser: true, isIncludeActivitySnapshotUser: true };
+  const { data: usernameData, error } = useSWRxUsernames(searchKeyword, 0, 5, requestOptions);
+  const activeUsernames = usernameData?.activeUser?.usernames != null ? usernameData.activeUser.usernames : [];
+  const inactiveUsernames = usernameData?.inactiveUser?.usernames != null ? usernameData.inactiveUser.usernames : [];
+  const activitySnapshotUsernames = usernameData?.activitySnapshotUser?.usernames != null ? usernameData.activitySnapshotUser.usernames : [];
+  const isLoading = usernameData === undefined && error == null;
+
+  const allUser: UserDataType[] = [];
+  const pushToAllUser = (usernames: string[], category: CategoryType) => {
+    usernames.forEach(username => allUser.push({ username, category }));
+  };
+  pushToAllUser(activeUsernames, Categories.activeUser);
+  pushToAllUser(inactiveUsernames, Categories.inactiveUser);
+  pushToAllUser(activitySnapshotUsernames, Categories.activitySnapshotUser);
+
+  /*
+   * Functions
+   */
+  const changeHandler = useCallback((userData: UserDataType[]) => {
+    if (onChange != null) {
+      const usernames = userData.map(user => user.username);
+      onChange(usernames);
+    }
+  }, [onChange]);
+
+  const searchHandler = useCallback((text: string) => {
+    setSearchKeyword(text);
+  }, []);
+
+  const renderMenu = useCallback((allUser: UserDataType[], menuProps) => {
+    if (allUser == null || allUser.length === 0) {
+      return <></>;
+    }
+
+    let index = 0;
+    const items = Object.values(Categories).map((category) => {
+      const userData = allUser.filter(user => user.category === category);
+      return (
+        <Fragment key={category}>
+          {index !== 0 && <Menu.Divider />}
+          <Menu.Header>{category}</Menu.Header>
+          {userData.map((user) => {
+            const item = (
+              <MenuItem key={index} option={user} position={index}>
+                {user.username}
+              </MenuItem>
+            );
+            index++;
+            return item;
+          })}
+        </Fragment>
+      );
+    });
+
+    return (
+      <Menu {...menuProps}>{items}</Menu>
+    );
+  }, []);
+
+  return (
+    <div className="input-group mr-2">
+      <div className="input-group-prepend">
+        <span className="input-group-text">
+          <i className="icon-people" />
+        </span>
+      </div>
+      <AsyncTypeahead
+        id="search-username-typeahead-asynctypeahead"
+        multiple
+        delay={400}
+        minLength={0}
+        placeholder={t('admin:audit_log_management.username')}
+        caseSensitive={false}
+        isLoading={isLoading}
+        options={allUser}
+        onSearch={searchHandler}
+        onChange={changeHandler}
+        renderMenu={renderMenu}
+        labelKey={(option: UserDataType) => `${option.username}`}
+      />
+    </div>
+  );
+};

+ 123 - 0
packages/app/src/components/Admin/AuditLog/SelectActionDropdown.tsx

@@ -0,0 +1,123 @@
+import React, { FC, useMemo, useCallback } from 'react';
+
+import { useTranslation } from 'react-i18next';
+
+import {
+  SupportedActionType, SupportedActionCategoryType, SupportedActionCategory,
+  PageActions, CommentActions, TagActions, ShareLinkActions, AttachmentActions, InAppNotificationActions, SearchActions, UserActions, AdminActions,
+} from '~/interfaces/activity';
+
+type Props = {
+  actionMap: Map<SupportedActionType, boolean>
+  availableActions: SupportedActionType[]
+  onChangeAction: (action: SupportedActionType) => void
+  onChangeMultipleAction: (actions: SupportedActionType[], isChecked: boolean) => void
+}
+
+export const SelectActionDropdown: FC<Props> = (props: Props) => {
+  const { t } = useTranslation();
+  const {
+    actionMap, availableActions, onChangeAction, onChangeMultipleAction,
+  } = props;
+
+  const dropdownItems = useMemo<Array<{actionCategory: SupportedActionCategoryType, actions: SupportedActionType[]}>>(() => {
+    return (
+      [
+        {
+          actionCategory: SupportedActionCategory.PAGE,
+          actions: PageActions.filter(action => availableActions.includes(action)),
+        },
+        {
+          actionCategory: SupportedActionCategory.COMMENT,
+          actions: CommentActions.filter(action => availableActions.includes(action)),
+        },
+        {
+          actionCategory: SupportedActionCategory.TAG,
+          actions: TagActions.filter(action => availableActions.includes(action)),
+        },
+        {
+          actionCategory: SupportedActionCategory.ATTACHMENT,
+          actions: AttachmentActions.filter(action => availableActions.includes(action)),
+        },
+        {
+          actionCategory: SupportedActionCategory.SHARE_LINK,
+          actions: ShareLinkActions.filter(action => availableActions.includes(action)),
+        },
+        {
+          actionCategory: SupportedActionCategory.IN_APP_NOTIFICATION,
+          actions: InAppNotificationActions.filter(action => availableActions.includes(action)),
+        },
+        {
+          actionCategory: SupportedActionCategory.SEARCH,
+          actions: SearchActions.filter(action => availableActions.includes(action)),
+        },
+        {
+          actionCategory: SupportedActionCategory.USER,
+          actions: UserActions.filter(action => availableActions.includes(action)),
+        },
+        {
+          actionCategory: SupportedActionCategory.ADMIN,
+          actions: AdminActions.filter(action => availableActions.includes(action)),
+        },
+      ]
+    );
+  }, [availableActions]).filter(item => item.actions.length !== 0);
+
+  const actionCheckboxChangedHandler = useCallback((action) => {
+    if (onChangeAction != null) {
+      onChangeAction(action);
+    }
+  }, [onChangeAction]);
+
+  const multipleActionCheckboxChangedHandler = useCallback((actions, isChecked) => {
+    if (onChangeMultipleAction != null) {
+      onChangeMultipleAction(actions, isChecked);
+    }
+  }, [onChangeMultipleAction]);
+
+  return (
+    <div className="btn-group mr-2 admin-audit-log">
+      <button className="btn btn-outline-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown">
+        <i className="fa fa-fw fa-bolt" />{t('admin:audit_log_management.action')}
+      </button>
+      <ul className="dropdown-menu select-action-dropdown" aria-labelledby="dropdownMenuButton">
+        {dropdownItems.map(item => (
+          <div key={item.actionCategory}>
+            <div className="dropdown-item">
+              <div className="form-group px-2 m-0">
+                <input
+                  type="checkbox"
+                  className="form-check-input"
+                  defaultChecked
+                  onChange={(e) => { multipleActionCheckboxChangedHandler(item.actions, e.target.checked) }}
+                />
+                <label className="form-check-label">{t(`admin:audit_log_action_category.${item.actionCategory}`)}</label>
+              </div>
+            </div>
+            {
+              item.actions.map(action => (
+                <div className="dropdown-item" key={action}>
+                  <div className="form-group px-4 m-0">
+                    <input
+                      type="checkbox"
+                      className="form-check-input"
+                      id={`checkbox${action}`}
+                      onChange={() => { actionCheckboxChangedHandler(action) }}
+                      checked={actionMap.get(action)}
+                    />
+                    <label
+                      className="form-check-label"
+                      htmlFor={`checkbox${action}`}
+                    >
+                      {t(`admin:audit_log_action.${action}`)}
+                    </label>
+                  </div>
+                </div>
+              ))
+            }
+          </div>
+        ))}
+      </ul>
+    </div>
+  );
+};

+ 181 - 0
packages/app/src/components/Admin/AuditLogManagement.tsx

@@ -0,0 +1,181 @@
+import React, { FC, useState, useCallback } from 'react';
+
+import { format } from 'date-fns';
+import { useTranslation } from 'react-i18next';
+
+import { toastError } from '~/client/util/apiNotification';
+import { SupportedActionType } from '~/interfaces/activity';
+import { useSWRxActivity } from '~/stores/activity';
+import { useAuditLogEnabled, useAuditLogAvailableActions } from '~/stores/context';
+
+import PaginationWrapper from '../PaginationWrapper';
+
+import { ActivityTable } from './AuditLog/ActivityTable';
+import { AuditLogDisableMode } from './AuditLog/AuditLogDisableMode';
+import { AuditLogSettings } from './AuditLog/AuditLogSettings';
+import { DateRangePicker } from './AuditLog/DateRangePicker';
+import { SearchUsernameTypeahead } from './AuditLog/SearchUsernameTypeahead';
+import { SelectActionDropdown } from './AuditLog/SelectActionDropdown';
+
+
+const formatDate = (date: Date | null) => {
+  if (date == null) {
+    return '';
+  }
+  return format(new Date(date), 'yyyy-MM-dd');
+};
+
+const PAGING_LIMIT = 10;
+
+export const AuditLogManagement: FC = () => {
+  const { t } = useTranslation();
+
+  const { data: auditLogAvailableActionsData } = useAuditLogAvailableActions();
+  const auditLogAvailableActions = auditLogAvailableActionsData != null ? auditLogAvailableActionsData : [];
+
+  /*
+   * State
+   */
+  const [isSettingPage, setIsSettingPage] = useState<boolean>(false);
+  const [activePage, setActivePage] = useState<number>(1);
+  const offset = (activePage - 1) * PAGING_LIMIT;
+  const [startDate, setStartDate] = useState<Date | null>(null);
+  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])),
+  );
+
+  /*
+   * Fetch
+   */
+  const selectedDate = { startDate: formatDate(startDate), endDate: formatDate(endDate) };
+  const selectedActionList = Array.from(actionMap.entries()).filter(v => v[1]).map(v => v[0]);
+  const searchFilter = { actions: selectedActionList, dates: selectedDate, usernames: selectedUsernames };
+
+  const { data: activityData, mutate: mutateActivity, error } = useSWRxActivity(PAGING_LIMIT, offset, searchFilter);
+  const activityList = activityData?.docs != null ? activityData.docs : [];
+  const totalActivityNum = activityData?.totalDocs != null ? activityData.totalDocs : 0;
+  const isLoading = activityData === undefined && error == null;
+
+  if (error != null) {
+    toastError('Failed to get Audit Log');
+  }
+
+  const { data: auditLogEnabled } = useAuditLogEnabled();
+
+  /*
+   * Functions
+   */
+  const setActivePageHandler = useCallback((selectedPageNum: number) => {
+    setActivePage(selectedPageNum);
+  }, []);
+
+  const datePickerChangedHandler = useCallback((dateList: Date[] | null[]) => {
+    setActivePage(1);
+    setStartDate(dateList[0]);
+    setEndDate(dateList[1]);
+  }, []);
+
+  const actionCheckboxChangedHandler = useCallback((action: SupportedActionType) => {
+    setActivePage(1);
+    actionMap.set(action, !actionMap.get(action));
+    setActionMap(new Map(actionMap.entries()));
+  }, [actionMap, setActionMap]);
+
+  const multipleActionCheckboxChangedHandler = useCallback((actions: SupportedActionType[], isChecked) => {
+    setActivePage(1);
+    actions.forEach(action => actionMap.set(action, isChecked));
+    setActionMap(new Map(actionMap.entries()));
+  }, [actionMap, setActionMap]);
+
+  const setUsernamesHandler = useCallback((usernames: string[]) => {
+    setActivePage(1);
+    setSelectedUsernames(usernames);
+  }, []);
+
+  const reloadButtonPushedHandler = useCallback(() => {
+    setActivePage(1);
+    mutateActivity();
+  }, [mutateActivity]);
+
+  // eslint-disable-next-line max-len
+  const activityCounter = `<b>${activityList.length === 0 ? 0 : offset + 1}</b> - <b>${(PAGING_LIMIT * activePage) - (PAGING_LIMIT - activityList.length)}</b> of <b>${totalActivityNum}<b/>`;
+
+  if (!auditLogEnabled) {
+    return <AuditLogDisableMode />;
+  }
+
+  return (
+    <div data-testid="admin-auditlog">
+      <button type="button" className="btn btn-outline-secondary mb-4" onClick={() => setIsSettingPage(!isSettingPage)}>
+        {
+          isSettingPage
+            ? <><i className="fa fa-hand-o-left mr-1" />{t('admin:audit_log_management.return')}</>
+            : <><i className="fa icon-settings mr-1" />{t('admin:audit_log_management.settings')}</>
+        }
+      </button>
+
+      <h2 className="admin-setting-header mb-3">
+        <span>
+          {isSettingPage ? t('AuditLog Settings') : t('AuditLog')}
+        </span>
+      </h2>
+
+      {isSettingPage ? (
+        <AuditLogSettings />
+      ) : (
+        <>
+          <div className="form-inline mb-3">
+            <SearchUsernameTypeahead
+              onChange={setUsernamesHandler}
+            />
+
+            <DateRangePicker
+              startDate={startDate}
+              endDate={endDate}
+              onChange={datePickerChangedHandler}
+            />
+
+            <SelectActionDropdown
+              actionMap={actionMap}
+              availableActions={auditLogAvailableActions}
+              onChangeAction={actionCheckboxChangedHandler}
+              onChangeMultipleAction={multipleActionCheckboxChangedHandler}
+            />
+
+            <button type="button" className="btn ml-auto grw-btn-reload" onClick={reloadButtonPushedHandler}>
+              <i className="icon icon-reload" />
+            </button>
+          </div>
+
+          <p
+            className="ml-2"
+            // eslint-disable-next-line react/no-danger
+            dangerouslySetInnerHTML={{ __html: activityCounter }}
+          />
+
+          { isLoading
+            ? (
+              <div className="text-muted text-center mb-5">
+                <i className="fa fa-2x fa-spinner fa-pulse mr-1" />
+              </div>
+            )
+            : (
+              <ActivityTable activityList={activityList} />
+            )
+          }
+
+          <PaginationWrapper
+            activePage={activePage}
+            changePage={setActivePageHandler}
+            totalItemsCount={totalActivityNum}
+            pagingLimit={PAGING_LIMIT}
+            align="center"
+            size="sm"
+          />
+        </>
+      )}
+    </div>
+  );
+};

+ 16 - 11
packages/app/src/components/Admin/Common/AdminNavigation.jsx

@@ -5,22 +5,22 @@
 import React from 'react';
 
 import { pathUtils } from '@growi/core';
-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';
+// import AppContainer from '~/client/services/AppContainer';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
+// import { withUnstatedContainers } from '../../UnstatedUtils';
 
 const AdminNavigation = (props) => {
   const { t } = useTranslation();
-  const { appContainer } = props;
+  // const { appContainer } = props;
   const pathname = window.location.pathname;
 
-  const growiCloudUri = appContainer.config.env.GROWI_CLOUD_URI;
-  const growiAppIdForGrowiCloud = appContainer.config.env.GROWI_APP_ID_FOR_GROWI_CLOUD;
+  // const growiCloudUri = appContainer.config.env.GROWI_CLOUD_URI;
+  // const growiAppIdForGrowiCloud = appContainer.config.env.GROWI_APP_ID_FOR_GROWI_CLOUD;
 
   // eslint-disable-next-line react/prop-types
   const MenuLabel = ({ menu }) => {
@@ -37,6 +37,8 @@ const AdminNavigation = (props) => {
       case 'users':                    return <><i className="icon-fw icon-user"></i>            { t('User_Management') }</>;
       case 'user-groups':              return <><i className="icon-fw icon-people"></i>          { t('UserGroup Management') }</>;
       case 'search':                   return <><i className="icon-fw icon-magnifier"></i>       { t('Full Text Search Management') }</>;
+      // TODO: Consider where to place the "AuditLog"
+      case 'audit-log':                return <><i className="icon-fw icon-feed"></i>            { t('AuditLog')}</>;
       case 'cloud':                    return <><i className="icon-fw icon-share-alt"></i>       { t('to_cloud_settings')} </>;
       default:                         return <><i className="icon-fw icon-home"></i>            { t('Wiki Management Home Page') }</>;
     }
@@ -86,7 +88,8 @@ const AdminNavigation = (props) => {
         <MenuLink menu="users"        isListGroupItems isActive={isActiveMenu('/users')} />
         <MenuLink menu="user-groups"  isListGroupItems isActive={isActiveMenu('/user-groups')} />
         <MenuLink menu="search"       isListGroupItems isActive={isActiveMenu('/search')} />
-        {growiCloudUri != null && growiAppIdForGrowiCloud != null
+        <MenuLink menu="audit-log"    isListGroupItems isActive={isActiveMenu('/audit-log')} />
+        {/* {growiCloudUri != null && growiAppIdForGrowiCloud != null
           && (
             <a
               href={`${growiCloudUri}/my/apps/${growiAppIdForGrowiCloud}`}
@@ -95,7 +98,7 @@ const AdminNavigation = (props) => {
               <MenuLabel menu="cloud" />
             </a>
           )
-        }
+        } */}
       </>
     );
   };
@@ -131,6 +134,7 @@ const AdminNavigation = (props) => {
             {isActiveMenu('/users') &&             <MenuLabel menu="users" />}
             {isActiveMenu('/user-groups') &&       <MenuLabel menu="user-groups" />}
             {isActiveMenu('/search') &&            <MenuLabel menu="search" />}
+            {isActiveMenu('/audit-log') &&         <MenuLabel menu="audit-log" />}
           </span>
         </button>
         <div className="dropdown-menu" aria-labelledby="dropdown-admin-navigation">
@@ -142,10 +146,11 @@ const AdminNavigation = (props) => {
   );
 };
 
-const AdminNavigationWrapper = withUnstatedContainers(AdminNavigation, [AppContainer]);
+// const AdminNavigationWrapper = withUnstatedContainers(AdminNavigation, [AppContainer]);
 
 AdminNavigation.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  // appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 };
 
-export default AdminNavigationWrapper;
+// export default AdminNavigationWrapper;
+export default AdminNavigation;

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

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

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

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

+ 17 - 16
packages/app/src/components/Admin/Customize/CustomizeThemeOptions.jsx

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

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

@@ -1,7 +1,7 @@
 import React from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 class StatusTable extends React.PureComponent {
 

+ 45 - 51
packages/app/src/components/Admin/ExportArchiveDataPage.jsx

@@ -1,17 +1,13 @@
-import React, { Fragment } from 'react';
+import React from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 import * as toastr from 'toastr';
 
 
-import AdminSocketIoContainer from '~/client/services/AdminSocketIoContainer';
-import AppContainer from '~/client/services/AppContainer';
-import { apiDelete, apiGet } from '~/client/util/apiv1-client';
-
-import { withUnstatedContainers } from '../UnstatedUtils';
 // import { toastSuccess, toastError } from '~/client/util/apiNotification';
-
+import { apiDelete, apiGet } from '~/client/util/apiv1-client';
+import { useAdminSocket } from '~/stores/socket-io';
 
 import LabeledProgressBar from './Common/LabeledProgressBar';
 import ArchiveFilesTable from './ExportArchiveData/ArchiveFilesTable';
@@ -70,45 +66,47 @@ class ExportArchiveDataPage extends React.Component {
   }
 
   setupWebsocketEventHandler() {
-    const socket = this.props.adminSocketIoContainer.getSocket();
-
-    // websocket event
-    socket.on('admin:onProgressForExport', ({ currentCount, totalCount, progressList }) => {
-      this.setState({
-        isExporting: true,
-        progressList,
-      });
-    });
-
-    // websocket event
-    socket.on('admin:onStartZippingForExport', () => {
-      this.setState({
-        isZipping: true,
+    const { socket } = this.props;
+
+    if (socket != null) {
+      // websocket event
+      socket.on('admin:onProgressForExport', ({ currentCount, totalCount, progressList }) => {
+        this.setState({
+          isExporting: true,
+          progressList,
+        });
       });
-    });
-
-    // websocket event
-    socket.on('admin:onTerminateForExport', ({ addedZipFileStat }) => {
-      const zipFileStats = this.state.zipFileStats.concat([addedZipFileStat]);
 
-      this.setState({
-        isExporting: false,
-        isZipping: false,
-        isExported: true,
-        zipFileStats,
+      // websocket event
+      socket.on('admin:onStartZippingForExport', () => {
+        this.setState({
+          isZipping: true,
+        });
       });
 
-      // TODO: toastSuccess, toastError
-      toastr.success(undefined, `New Archive Data '${addedZipFileStat.fileName}' is added`, {
-        closeButton: true,
-        progressBar: true,
-        newestOnTop: false,
-        showDuration: '100',
-        hideDuration: '100',
-        timeOut: '1200',
-        extendedTimeOut: '150',
+      // websocket event
+      socket.on('admin:onTerminateForExport', ({ addedZipFileStat }) => {
+        const zipFileStats = this.state.zipFileStats.concat([addedZipFileStat]);
+
+        this.setState({
+          isExporting: false,
+          isZipping: false,
+          isExported: true,
+          zipFileStats,
+        });
+
+        // TODO: toastSuccess, toastError
+        toastr.success(undefined, `New Archive Data '${addedZipFileStat.fileName}' is added`, {
+          closeButton: true,
+          progressBar: true,
+          newestOnTop: false,
+          showDuration: '100',
+          hideDuration: '100',
+          timeOut: '1200',
+          extendedTimeOut: '150',
+        });
       });
-    });
+    }
   }
 
   onZipFileStatAdd(newStat) {
@@ -250,18 +248,14 @@ class ExportArchiveDataPage extends React.Component {
 
 ExportArchiveDataPage.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminSocketIoContainer: PropTypes.instanceOf(AdminSocketIoContainer).isRequired,
+  socket: PropTypes.object,
 };
 
 const ExportArchiveDataPageWrapperFC = (props) => {
   const { t } = useTranslation();
-  return <ExportArchiveDataPage t={t} {...props} />;
-};
+  const { data: socket } = useAdminSocket();
 
-/**
- * Wrapper component for using unstated
- */
-const ExportArchiveDataPageWrapper = withUnstatedContainers(ExportArchiveDataPageWrapperFC, [AppContainer, AdminSocketIoContainer]);
+  return <ExportArchiveDataPage t={t} socket={socket} {...props} />;
+};
 
-export default ExportArchiveDataPageWrapper;
+export default ExportArchiveDataPageWrapperFC;

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

@@ -1,14 +1,14 @@
 import React, { Fragment } from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 import * as toastr from 'toastr';
 
 import { apiv3Delete, apiv3Get } from '~/client/util/apiv3-client';
 
 // import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
-import ImportForm from './GrowiArchive/ImportForm';
+// import ImportForm from './GrowiArchive/ImportForm';
 import UploadForm from './GrowiArchive/UploadForm';
 
 class GrowiArchiveSection extends React.Component {
@@ -129,11 +129,12 @@ class GrowiArchiveSection extends React.Component {
         {isTheSameVersion === false && this.renderDefferentVersionAlert()}
         {this.state.fileName != null && isTheSameVersion === true ? (
           <div className="px-4">
-            <ImportForm
+            {/* show ImportForm by https://redmine.weseek.co.jp/issues/100061 */}
+            {/* <ImportForm
               fileName={this.state.fileName}
               innerFileStats={this.state.innerFileStats}
               onDiscard={this.discardData}
-            />
+            /> */}
           </div>
         )
           : (

+ 1 - 1
packages/app/src/components/Admin/ImportData/ImportDataPageContents.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 AdminImportContainer from '~/client/services/AdminImportContainer';
 

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

@@ -17,7 +17,6 @@ class ManageExternalAccount extends React.Component {
 
   constructor(props) {
     super(props);
-    this.xss = window.xss;
     this.handleExternalAccountPage = this.handleExternalAccountPage.bind(this);
   }
 

+ 2 - 4
packages/app/src/components/Admin/MarkdownSetting/PresentationForm.jsx

@@ -1,10 +1,9 @@
 import React from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import loggerFactory from '~/utils/logger';
 
@@ -129,7 +128,6 @@ class PresentationForm extends React.Component {
 
 PresentationForm.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
 
 };
@@ -140,6 +138,6 @@ const PresentationFormWrapperFC = (props) => {
   return <PresentationForm t={t} {...props} />;
 };
 
-const PresentationFormWrapper = withUnstatedContainers(PresentationFormWrapperFC, [AppContainer, AdminMarkDownContainer]);
+const PresentationFormWrapper = withUnstatedContainers(PresentationFormWrapperFC, [AdminMarkDownContainer]);
 
 export default PresentationFormWrapper;

+ 2 - 4
packages/app/src/components/Admin/MarkdownSetting/WhiteListInput.jsx

@@ -1,10 +1,9 @@
 import React from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { tags, attrs } from '~/services/xss/recommended-whitelist';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -79,7 +78,6 @@ class WhiteListInput extends React.Component {
 
 WhiteListInput.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
 
 };
@@ -90,6 +88,6 @@ const PresentationFormWrapperFC = (props) => {
   return <WhiteListInput t={t} {...props} />;
 };
 
-const WhiteListWrapper = withUnstatedContainers(PresentationFormWrapperFC, [AppContainer, AdminMarkDownContainer]);
+const WhiteListWrapper = withUnstatedContainers(PresentationFormWrapperFC, [AdminMarkDownContainer]);
 
 export default WhiteListWrapper;

+ 2 - 4
packages/app/src/components/Admin/MarkdownSetting/XssForm.jsx

@@ -1,10 +1,9 @@
 import React from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { tags, attrs } from '~/services/xss/recommended-whitelist';
 import loggerFactory from '~/utils/logger';
@@ -165,7 +164,6 @@ class XssForm extends React.Component {
 
 XssForm.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
 };
 
@@ -175,6 +173,6 @@ const XssFormWrapperFC = (props) => {
   return <XssForm t={t} {...props} />;
 };
 
-const XssFormWrapper = withUnstatedContainers(XssFormWrapperFC, [AppContainer, AdminMarkDownContainer]);
+const XssFormWrapper = withUnstatedContainers(XssFormWrapperFC, [AdminMarkDownContainer]);
 
 export default XssFormWrapper;

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

@@ -1,10 +1,9 @@
 import React from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import loggerFactory from '~/utils/logger';
 
@@ -130,7 +129,6 @@ class GlobalNotification extends React.Component {
 
 GlobalNotification.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer).isRequired,
 
 };
@@ -141,6 +139,6 @@ const GlobalNotificationWrapperFC = (props) => {
   return <GlobalNotification t={t} {...props} />;
 };
 
-const GlobalNotificationWrapper = withUnstatedContainers(GlobalNotificationWrapperFC, [AppContainer, AdminNotificationContainer]);
+const GlobalNotificationWrapper = withUnstatedContainers(GlobalNotificationWrapperFC, [AdminNotificationContainer]);
 
 export default GlobalNotificationWrapper;

+ 2 - 4
packages/app/src/components/Admin/Notification/GlobalNotificationList.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 urljoin from 'url-join';
 
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Put } from '~/client/util/apiv3-client';
 import loggerFactory from '~/utils/logger';
@@ -176,7 +175,6 @@ class GlobalNotificationList extends React.Component {
 
 GlobalNotificationList.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer).isRequired,
 
 };
@@ -187,6 +185,6 @@ const GlobalNotificationListWrapperFC = (props) => {
   return <GlobalNotificationList t={t} {...props} />;
 };
 
-const GlobalNotificationListWrapper = withUnstatedContainers(GlobalNotificationListWrapperFC, [AppContainer, AdminNotificationContainer]);
+const GlobalNotificationListWrapper = withUnstatedContainers(GlobalNotificationListWrapperFC, [AdminNotificationContainer]);
 
 export default GlobalNotificationListWrapper;

+ 9 - 12
packages/app/src/components/Admin/Notification/NotificationSetting.jsx

@@ -1,28 +1,25 @@
 import React, {
   useCallback, useEffect, useMemo, useState,
 } from 'react';
-import PropTypes from 'prop-types';
 
+import { SlackbotType } from '@growi/slack';
+import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 import {
   TabContent, TabPane,
 } from 'reactstrap';
-import { useTranslation } from 'next-i18next';
-
-import { SlackbotType } from '@growi/slack';
-
-import loggerFactory from '~/utils/logger';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
+import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
 import { toastError } from '~/client/util/apiNotification';
 import { toArrayIfNot } from '~/utils/array-utils';
-import { withLoadingSppiner } from '../../SuspenseUtils';
-
-import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
+import loggerFactory from '~/utils/logger';
 
 import { CustomNavTab } from '../../CustomNavigation/CustomNav';
+import { withUnstatedContainers } from '../../UnstatedUtils';
+
 
-import UserTriggerNotification from './UserTriggerNotification';
 import GlobalNotification from './GlobalNotification';
+import UserTriggerNotification from './UserTriggerNotification';
 
 const logger = loggerFactory('growi:NotificationSetting');
 
@@ -174,7 +171,7 @@ function NotificationSetting(props) {
   );
 }
 
-const NotificationSettingWithUnstatedContainer = withUnstatedContainers(withLoadingSppiner(NotificationSetting), [AdminNotificationContainer]);
+const NotificationSettingWithUnstatedContainer = withUnstatedContainers(NotificationSetting, [AdminNotificationContainer]);
 
 NotificationSetting.propTypes = {
   adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer).isRequired,

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

@@ -1,10 +1,9 @@
 import React from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
-import AppContainer from '~/client/services/AppContainer';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
@@ -38,7 +37,6 @@ class UserNotificationRow extends React.PureComponent {
 
 UserNotificationRow.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer).isRequired,
 
   notification: PropTypes.object.isRequired,
@@ -52,7 +50,7 @@ const UserNotificationRowWrapperWrapperFC = (props) => {
   return <UserNotificationRow t={t} {...props} />;
 };
 
-const UserNotificationRowWrapper = withUnstatedContainers(UserNotificationRowWrapperWrapperFC, [AppContainer, AdminNotificationContainer]);
+const UserNotificationRowWrapper = withUnstatedContainers(UserNotificationRowWrapperWrapperFC, [AdminNotificationContainer]);
 
 
 export default UserNotificationRowWrapper;

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

@@ -1,10 +1,9 @@
 import React from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import loggerFactory from '~/utils/logger';
 
@@ -148,7 +147,6 @@ class UserTriggerNotification extends React.Component {
 
 UserTriggerNotification.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminNotificationContainer: PropTypes.instanceOf(AdminNotificationContainer).isRequired,
 
 };
@@ -159,6 +157,6 @@ const UserTriggerNotificationWrapperFC = (props) => {
   return <UserTriggerNotification t={t} {...props} />;
 };
 
-const UserTriggerNotificationWrapper = withUnstatedContainers(UserTriggerNotificationWrapperFC, [AppContainer, AdminNotificationContainer]);
+const UserTriggerNotificationWrapper = withUnstatedContainers(UserTriggerNotificationWrapperFC, [AdminNotificationContainer]);
 
 export default UserTriggerNotificationWrapper;

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

@@ -1,12 +1,13 @@
 /* eslint-disable react/no-danger */
 import React from 'react';
-import PropTypes from 'prop-types';
+
 import { withTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
+
+import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
-import AppContainer from '~/client/services/AppContainer';
-import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 
 class FacebookSecurityManagement extends React.Component {
 
@@ -30,10 +31,9 @@ class FacebookSecurityManagement extends React.Component {
 
 FacebookSecurityManagement.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
 };
 
-const TwitterSecurityManagementWrapper = withUnstatedContainers(FacebookSecurityManagement, [AppContainer, AdminGeneralSecurityContainer]);
+const TwitterSecurityManagementWrapper = withUnstatedContainers(FacebookSecurityManagement, [AdminGeneralSecurityContainer]);
 
 export default withTranslation()(TwitterSecurityManagementWrapper);

+ 2 - 0
packages/app/src/components/Admin/Security/LdapAuthTest.jsx

@@ -97,6 +97,7 @@ class LdapAuthTest extends React.Component {
               name="username"
               value={this.props.username}
               onChange={(e) => { this.props.onChangeUsername(e.target.value) }}
+              autoComplete="off"
             />
           </div>
         </div>
@@ -109,6 +110,7 @@ class LdapAuthTest extends React.Component {
               name="password"
               value={this.props.password}
               onChange={(e) => { this.props.onChangePassword(e.target.value) }}
+              autoComplete="off"
             />
           </div>
         </div>

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

@@ -1,21 +1,21 @@
-import React, { Fragment, useMemo, useState } from 'react';
+import React, { useMemo, useState } from 'react';
 
 import { useTranslation } from 'next-i18next';
 import { TabContent, TabPane } from 'reactstrap';
 
 import CustomNav from '../../CustomNavigation/CustomNav';
 
-import BasicSecuritySetting from './BasicSecuritySetting';
-import FacebookSecuritySetting from './FacebookSecuritySetting';
-import GitHubSecuritySetting from './GitHubSecuritySetting';
-import GoogleSecuritySetting from './GoogleSecuritySetting';
-import LdapSecuritySetting from './LdapSecuritySetting';
-import LocalSecuritySetting from './LocalSecuritySetting';
-import OidcSecuritySetting from './OidcSecuritySetting';
-import SamlSecuritySetting from './SamlSecuritySetting';
+// import BasicSecuritySetting from './BasicSecuritySetting';
+// import FacebookSecuritySetting from './FacebookSecuritySetting';
+// import GitHubSecuritySetting from './GitHubSecuritySetting';
+// import GoogleSecuritySetting from './GoogleSecuritySetting';
+// import LdapSecuritySetting from './LdapSecuritySetting';
+// import LocalSecuritySetting from './LocalSecuritySetting';
+// import OidcSecuritySetting from './OidcSecuritySetting';
+// import SamlSecuritySetting from './SamlSecuritySetting';
 import SecuritySetting from './SecuritySetting';
 import ShareLinkSetting from './ShareLinkSetting';
-import TwitterSecuritySetting from './TwitterSecuritySetting';
+// import TwitterSecuritySetting from './TwitterSecuritySetting';
 
 const SecurityManagementContents = () => {
   const { t } = useTranslation();
@@ -82,12 +82,12 @@ const SecurityManagementContents = () => {
   return (
     <div data-testid="admin-security">
       <div className="mb-5">
-        <SecuritySetting />
+        {/* <SecuritySetting /> */}
       </div>
 
       {/* Shared Link List */}
       <div className="mb-5">
-        <ShareLinkSetting />
+        {/* <ShareLinkSetting /> */}
       </div>
 
 
@@ -112,31 +112,31 @@ const SecurityManagementContents = () => {
         />
         <TabContent activeTab={activeTab} className="p-5">
           <TabPane tabId="passport_local">
-            {activeComponents.has('passport_local') && <LocalSecuritySetting />}
+            {/* {activeComponents.has('passport_local') && <LocalSecuritySetting />} */}
           </TabPane>
           <TabPane tabId="passport_ldap">
-            {activeComponents.has('passport_ldap') && <LdapSecuritySetting />}
+            {/* {activeComponents.has('passport_ldap') && <LdapSecuritySetting />} */}
           </TabPane>
           <TabPane tabId="passport_saml">
-            {activeComponents.has('passport_saml') && <SamlSecuritySetting />}
+            {/* {activeComponents.has('passport_saml') && <SamlSecuritySetting />} */}
           </TabPane>
           <TabPane tabId="passport_oidc">
-            {activeComponents.has('passport_oidc') && <OidcSecuritySetting />}
+            {/* {activeComponents.has('passport_oidc') && <OidcSecuritySetting />} */}
           </TabPane>
           <TabPane tabId="passport_basic">
-            {activeComponents.has('passport_basic') && <BasicSecuritySetting />}
+            {/* {activeComponents.has('passport_basic') && <BasicSecuritySetting />} */}
           </TabPane>
           <TabPane tabId="passport_google">
-            {activeComponents.has('passport_google') && <GoogleSecuritySetting />}
+            {/* {activeComponents.has('passport_google') && <GoogleSecuritySetting />} */}
           </TabPane>
           <TabPane tabId="passport_github">
-            {activeComponents.has('passport_github') && <GitHubSecuritySetting />}
+            {/* {activeComponents.has('passport_github') && <GitHubSecuritySetting />} */}
           </TabPane>
           <TabPane tabId="passport_twitter">
-            {activeComponents.has('passport_twitter') && <TwitterSecuritySetting />}
+            {/* {activeComponents.has('passport_twitter') && <TwitterSecuritySetting />} */}
           </TabPane>
           <TabPane tabId="passport_facebook">
-            {activeComponents.has('passport_facebook') && <FacebookSecuritySetting />}
+            {/* {activeComponents.has('passport_facebook') && <FacebookSecuritySetting />} */}
           </TabPane>
         </TabContent>
       </div>

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

@@ -1,10 +1,9 @@
 import React, { Fragment } from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Delete } from '~/client/util/apiv3-client';
 
@@ -78,7 +77,7 @@ class ShareLinkSetting extends React.Component {
   }
 
   async deleteAllLinksButtonHandler() {
-    const { t, appContainer } = this.props;
+    const { t } = this.props;
 
     try {
       const res = await apiv3Delete('/share-links/all');
@@ -92,7 +91,7 @@ class ShareLinkSetting extends React.Component {
   }
 
   async deleteLinkById(shareLinkId) {
-    const { t, appContainer, adminGeneralSecurityContainer } = this.props;
+    const { t, adminGeneralSecurityContainer } = this.props;
     const { shareLinksActivePage } = adminGeneralSecurityContainer.state;
 
     try {
@@ -193,7 +192,6 @@ class ShareLinkSetting extends React.Component {
 
 ShareLinkSetting.propTypes = {
   t: PropTypes.func.isRequired, //  i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
 };
 
@@ -205,6 +203,6 @@ const ShareLinkSettingWrapperFC = (props) => {
 /**
  * Wrapper component for using unstated
  */
-const ShareLinkSettingWrapper = withUnstatedContainers(ShareLinkSettingWrapperFC, [AppContainer, AdminGeneralSecurityContainer]);
+const ShareLinkSettingWrapper = withUnstatedContainers(ShareLinkSettingWrapperFC, [AdminGeneralSecurityContainer]);
 
 export default ShareLinkSettingWrapper;

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

@@ -9,7 +9,7 @@ import { Tooltip } from 'reactstrap';
 
 import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
-import { apiv3Put } from '~/client/util/apiv3-client';
+import { apiv3Put, apiv3Post } from '~/client/util/apiv3-client';
 import loggerFactory from '~/utils/logger';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -234,7 +234,7 @@ const GeneratingTokensAndRegisteringProxyServiceProcess = withUnstatedContainers
 }, [AppContainer]);
 
 const TestProcess = ({
-  apiv3Post, slackAppIntegrationId, onSubmitForm, onSubmitFormFailed, isLatestConnectionSuccess,
+  slackAppIntegrationId, onSubmitForm, onSubmitFormFailed, isLatestConnectionSuccess,
 }) => {
 
   const { t } = useTranslation();
@@ -353,7 +353,6 @@ const WithProxyAccordions = (props) => {
     '④': {
       title: 'test_connection',
       content: <TestProcess
-        apiv3Post={props.apiv3Post}
         slackAppIntegrationId={props.slackAppIntegrationId}
         onSubmitForm={submitForm}
         onSubmitFormFailed={submitFormFailed}
@@ -397,7 +396,6 @@ const WithProxyAccordions = (props) => {
     '⑥': {
       title: 'test_connection',
       content: <TestProcess
-        apiv3Post={props.apiv3Post}
         slackAppIntegrationId={props.slackAppIntegrationId}
         onSubmitForm={submitForm}
         onSubmitFormFailed={submitFormFailed}

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

@@ -1,6 +1,7 @@
 import React, {
   FC, useCallback, useState, useMemo,
 } from 'react';
+
 import { TFunctionResult } from 'i18next';
 import { useTranslation } from 'next-i18next';
 import {
@@ -8,8 +9,6 @@ import {
 } from 'reactstrap';
 
 import { IUserGroupHasId } from '~/interfaces/user';
-import { CustomWindow } from '~/interfaces/global';
-import Xss from '~/services/xss';
 
 /**
  * Delete User Group Select component
@@ -42,10 +41,13 @@ const actionForPages = {
 };
 
 const UserGroupDeleteModal: FC<Props> = (props: Props) => {
-  const xss: Xss = (window as CustomWindow).xss;
 
   const { t } = useTranslation();
 
+  const {
+    onHide, onDelete, userGroups, deleteUserGroup,
+  } = props;
+
   const availableOptions = useMemo<AvailableOption[]>(() => {
     return [
       {
@@ -70,7 +72,7 @@ const UserGroupDeleteModal: FC<Props> = (props: Props) => {
         label: t('admin:user_group_management.delete_modal.transfer_pages'),
       },
     ];
-  }, []);
+  }, [t]);
 
   /*
    * State
@@ -86,14 +88,14 @@ const UserGroupDeleteModal: FC<Props> = (props: Props) => {
     setTransferToUserGroupId('');
   }, []);
 
-  const onHide = useCallback(() => {
-    if (props.onHide == null) {
+  const toggleHandler = useCallback(() => {
+    if (onHide == null) {
       return;
     }
 
     resetStates();
-    props.onHide();
-  }, [props.onHide]);
+    onHide();
+  }, [onHide, resetStates]);
 
   const handleActionChange = useCallback((e) => {
     const actionName = e.target.value;
@@ -106,23 +108,22 @@ const UserGroupDeleteModal: FC<Props> = (props: Props) => {
   }, []);
 
   const handleSubmit = useCallback((e) => {
-    if (props.onDelete == null || props.deleteUserGroup == null) {
+    if (onDelete == null || deleteUserGroup == null) {
       return;
     }
 
     e.preventDefault();
 
-    props.onDelete(
-      props.deleteUserGroup._id,
+    onDelete(
+      deleteUserGroup._id,
       actionName,
       transferToUserGroupId,
     );
-  }, [props.onDelete, props.deleteUserGroup, actionName, transferToUserGroupId]);
+  }, [onDelete, deleteUserGroup, actionName, transferToUserGroupId]);
 
   const renderPageActionSelector = useCallback(() => {
     const options = availableOptions.map((opt) => {
-      const dataContent = `<i class="icon icon-fw ${opt.iconClass} ${opt.styleClass}"></i> <span class="action-name ${opt.styleClass}">${opt.label}</span>`;
-      return <option key={opt.id} value={opt.actionForPages} data-content={dataContent}>{opt.label}</option>;
+      return <option key={opt.id} value={opt.actionForPages}>{opt.label}</option>;
     });
 
     return (
@@ -134,25 +135,22 @@ const UserGroupDeleteModal: FC<Props> = (props: Props) => {
         onChange={handleActionChange}
       >
         <option value="" disabled>{t('admin:user_group_management.delete_modal.dropdown_desc')}</option>
-        {options}
+        {...options}
       </select>
     );
-  }, [handleActionChange, actionName, availableOptions]);
+  }, [availableOptions, actionName, handleActionChange, t]);
 
   const renderGroupSelector = useCallback(() => {
-    const { deleteUserGroup } = props;
-
     if (deleteUserGroup == null) {
       return;
     }
 
-    const groups = props.userGroups.filter((group) => {
+    const groups = userGroups.filter((group) => {
       return group._id !== deleteUserGroup._id;
     });
 
     const options = groups.map((group) => {
-      const dataContent = `<i class="icon icon-fw icon-organization"></i> ${xss.process(group.name)}`;
-      return <option key={group._id} value={group._id} data-content={dataContent}>{xss.process(group.name)}</option>;
+      return <option key={group._id} value={group._id}>{group.name}</option>;
     });
 
     const defaultOptionText = groups.length === 0 ? t('admin:user_group_management.delete_modal.no_groups')
@@ -166,10 +164,10 @@ const UserGroupDeleteModal: FC<Props> = (props: Props) => {
         onChange={handleGroupChange}
       >
         <option value="" disabled>{defaultOptionText}</option>
-        {options}
+        {...options}
       </select>
     );
-  }, [actionName, transferToUserGroupId, props.userGroups, props.deleteUserGroup]);
+  }, [deleteUserGroup, userGroups, t, actionName, transferToUserGroupId, handleGroupChange]);
 
   const validateForm = useCallback(() => {
     let isValid = true;
@@ -185,8 +183,8 @@ const UserGroupDeleteModal: FC<Props> = (props: Props) => {
   }, [actionName, transferToUserGroupId]);
 
   return (
-    <Modal className="modal-md" isOpen={props.isShow} toggle={onHide}>
-      <ModalHeader tag="h4" toggle={onHide} className="bg-danger text-light">
+    <Modal className="modal-md" isOpen={props.isShow} toggle={toggleHandler}>
+      <ModalHeader tag="h4" toggle={toggleHandler} className="bg-danger text-light">
         <i className="icon icon-fire"></i> {t('admin:user_group_management.delete_modal.header')}
       </ModalHeader>
       <ModalBody>

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

@@ -1,11 +1,10 @@
 import React, { FC, useCallback, useState } from 'react';
-import { useTranslation } from 'next-i18next';
+
 import dateFnsFormat from 'date-fns/format';
 import { TFunctionResult } from 'i18next';
+import { useTranslation } from 'next-i18next';
 
 import { IUserGroupHasId } from '~/interfaces/user';
-import { CustomWindow } from '~/interfaces/global';
-import Xss from '~/services/xss';
 
 type Props = {
   userGroup: IUserGroupHasId,
@@ -15,7 +14,6 @@ type Props = {
 };
 
 const UserGroupForm: FC<Props> = (props: Props) => {
-  const xss: Xss = (window as CustomWindow).xss;
 
   const { t } = useTranslation();
 

+ 4 - 6
packages/app/src/components/Admin/UserGroup/UserGroupModal.tsx

@@ -1,16 +1,15 @@
 import React, {
   FC, useState, useEffect, useCallback,
 } from 'react';
+
+import { Ref } from '@growi/core';
+import { TFunctionResult } from 'i18next';
+import { useTranslation } from 'next-i18next';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
-import { useTranslation } from 'next-i18next';
-import { TFunctionResult } from 'i18next';
 
-import { Ref } from '~/interfaces/common';
 import { IUserGroup, IUserGroupHasId } from '~/interfaces/user';
-import { CustomWindow } from '~/interfaces/global';
-import Xss from '~/services/xss';
 
 type Props = {
   userGroup?: IUserGroupHasId,
@@ -21,7 +20,6 @@ type Props = {
 };
 
 const UserGroupModal: FC<Props> = (props: Props) => {
-  const xss: Xss = (window as CustomWindow).xss;
 
   const { t } = useTranslation();
 

+ 8 - 9
packages/app/src/components/Admin/UserGroup/UserGroupPage.tsx

@@ -1,20 +1,19 @@
 import React, { FC, useState, useCallback } from 'react';
-import { useTranslation } from 'next-i18next';
 
-import UserGroupTable from './UserGroupTable';
-import UserGroupModal from './UserGroupModal';
-import UserGroupDeleteModal from './UserGroupDeleteModal';
+import { useTranslation } from 'react-i18next';
+
 
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
-import { IUserGroup, IUserGroupHasId } from '~/interfaces/user';
-import Xss from '~/services/xss';
-import { CustomWindow } from '~/interfaces/global';
 import { apiv3Delete, apiv3Post, apiv3Put } from '~/client/util/apiv3-client';
-import { useSWRxUserGroupList, useSWRxChildUserGroupList, useSWRxUserGroupRelationList } from '~/stores/user-group';
+import { IUserGroup, IUserGroupHasId } from '~/interfaces/user';
 import { useIsAclEnabled } from '~/stores/context';
+import { useSWRxUserGroupList, useSWRxChildUserGroupList, useSWRxUserGroupRelationList } from '~/stores/user-group';
+
+import UserGroupDeleteModal from './UserGroupDeleteModal';
+import UserGroupModal from './UserGroupModal';
+import UserGroupTable from './UserGroupTable';
 
 const UserGroupPage: FC = () => {
-  const xss: Xss = (window as CustomWindow).xss;
   const { t } = useTranslation();
 
   const { data: isAclEnabled } = useIsAclEnabled();

+ 9 - 12
packages/app/src/components/Admin/UserGroup/UserGroupTable.tsx

@@ -1,14 +1,12 @@
 import React, {
-  FC, useState, useCallback, useEffect,
+  FC, useState, useEffect,
 } from 'react';
 
-import { useTranslation } from 'next-i18next';
-import { TFunctionResult } from 'i18next';
 import dateFnsFormat from 'date-fns/format';
+import { TFunctionResult } from 'i18next';
+import { useTranslation } from 'next-i18next';
 
-import Xss from '~/services/xss';
 import { IUserGroupHasId, IUserGroupRelation, IUserHasId } from '~/interfaces/user';
-import { CustomWindow } from '~/interfaces/global';
 
 type Props = {
   headerLabel?: TFunctionResult,
@@ -56,7 +54,6 @@ const generateGroupIdToChildGroupsMap = (childUserGroups: IUserGroupHasId[]): Re
 
 
 const UserGroupTable: FC<Props> = (props: Props) => {
-  const xss: Xss = (window as CustomWindow).xss;
   const { t } = useTranslation();
 
   /*
@@ -151,17 +148,17 @@ const UserGroupTable: FC<Props> = (props: Props) => {
               <tr key={group._id}>
                 {props.isAclEnabled
                   ? (
-                    <td><a href={`/admin/user-group-detail/${group._id}`}>{xss.process(group.name)}</a></td>
+                    <td><a href={`/admin/user-group-detail/${group._id}`}>{group.name}</a></td>
                   )
                   : (
-                    <td>{xss.process(group.name)}</td>
+                    <td>{group.name}</td>
                   )
                 }
-                <td>{xss.process(group.description)}</td>
+                <td>{group.description}</td>
                 <td>
                   <ul className="list-inline">
                     {users != null && users.map((user) => {
-                      return <li key={user._id} className="list-inline-item badge badge-pill badge-warning">{xss.process(user.username)}</li>;
+                      return <li key={user._id} className="list-inline-item badge badge-pill badge-warning">{user.username}</li>;
                     })}
                   </ul>
                 </td>
@@ -172,10 +169,10 @@ const UserGroupTable: FC<Props> = (props: Props) => {
                         <li key={group._id} className="list-inline-item badge badge-success">
                           {props.isAclEnabled
                             ? (
-                              <a href={`/admin/user-group-detail/${group._id}`}>{xss.process(group.name)}</a>
+                              <a href={`/admin/user-group-detail/${group._id}`}>{group.name}</a>
                             )
                             : (
-                              <p>{xss.process(group.name)}</p>
+                              <p>{group.name}</p>
                             )
                           }
                         </li>

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

@@ -9,6 +9,7 @@ import { debounce } from 'throttle-debounce';
 import AdminUserGroupDetailContainer from '~/client/services/AdminUserGroupDetailContainer';
 import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import Xss from '~/services/xss';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
@@ -25,7 +26,7 @@ class UserGroupUserFormByInput extends React.Component {
       searchError: null,
     };
 
-    this.xss = window.xss;
+    this.xss = new Xss();
 
     this.addUserBySubmit = this.addUserBySubmit.bind(this);
     this.validateForm = this.validateForm.bind(this);

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

@@ -8,6 +8,7 @@ import { useTranslation } from 'next-i18next';
 import AdminUserGroupDetailContainer from '~/client/services/AdminUserGroupDetailContainer';
 import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import Xss from '~/services/xss';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
@@ -16,7 +17,7 @@ class UserGroupUserTable extends React.Component {
   constructor(props) {
     super(props);
 
-    this.xss = window.xss;
+    this.xss = new Xss();
 
     this.removeUser = this.removeUser.bind(this);
   }

+ 4 - 3
packages/app/src/components/Admin/UserManagement.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 AdminUsersContainer from '~/client/services/AdminUsersContainer';
 import { toastError } from '~/client/util/apiNotification';
@@ -10,7 +10,7 @@ import PaginationWrapper from '../PaginationWrapper';
 import { withUnstatedContainers } from '../UnstatedUtils';
 
 
-import InviteUserControl from './Users/InviteUserControl';
+// import InviteUserControl from './Users/InviteUserControl';
 import PasswordResetModal from './Users/PasswordResetModal';
 import UserTable from './Users/UserTable';
 
@@ -150,7 +150,8 @@ class UserManagement extends React.Component {
           />
         )}
         <p>
-          <InviteUserControl />
+          {/* show  */}
+          {/* <InviteUserControl /> */}
           <a className="btn btn-outline-secondary ml-2" href="/admin/users/external-accounts" role="button">
             <i className="icon-user-follow" aria-hidden="true"></i>
             {t('admin:user_management.external_account')}

+ 2 - 4
packages/app/src/components/Admin/Users/GiveAdminButton.jsx

@@ -1,10 +1,9 @@
 import React from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 import AdminUsersContainer from '~/client/services/AdminUsersContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -49,11 +48,10 @@ const GiveAdminButtonWrapperFC = (props) => {
 /**
  * Wrapper component for using unstated
  */
-const GiveAdminButtonWrapper = withUnstatedContainers(GiveAdminButtonWrapperFC, [AppContainer, AdminUsersContainer]);
+const GiveAdminButtonWrapper = withUnstatedContainers(GiveAdminButtonWrapperFC, [AdminUsersContainer]);
 
 GiveAdminButton.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminUsersContainer: PropTypes.instanceOf(AdminUsersContainer).isRequired,
 
   user: PropTypes.object.isRequired,

+ 2 - 4
packages/app/src/components/Admin/Users/InviteUserControl.jsx

@@ -1,10 +1,9 @@
 import React, { Fragment } from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 import AdminUsersContainer from '~/client/services/AdminUsersContainer';
-import AppContainer from '~/client/services/AppContainer';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
@@ -29,7 +28,6 @@ class InviteUserControl extends React.Component {
 
 InviteUserControl.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminUsersContainer: PropTypes.instanceOf(AdminUsersContainer).isRequired,
 };
 
@@ -41,6 +39,6 @@ const InviteUserControlWrapperFC = (props) => {
 /**
  * Wrapper component for using unstated
  */
-const InviteUserControlWrapper = withUnstatedContainers(InviteUserControlWrapperFC, [AppContainer, AdminUsersContainer]);
+const InviteUserControlWrapper = withUnstatedContainers(InviteUserControlWrapperFC, [AdminUsersContainer]);
 
 export default InviteUserControlWrapper;

+ 3 - 6
packages/app/src/components/Admin/Users/PasswordResetModal.jsx

@@ -1,12 +1,11 @@
 import React from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 
-import AppContainer from '~/client/services/AppContainer';
 import { toastError } from '~/client/util/apiNotification';
 import { apiv3Put } from '~/client/util/apiv3-client';
 
@@ -26,7 +25,7 @@ class PasswordResetModal extends React.Component {
   }
 
   async resetPassword() {
-    const { t, appContainer, userForPasswordResetModal } = this.props;
+    const { t, userForPasswordResetModal } = this.props;
     try {
       const res = await apiv3Put('/users/reset-password', { id: userForPasswordResetModal._id });
       const { newPassword } = res.data;
@@ -117,11 +116,9 @@ const PasswordResetModalWrapperFC = (props) => {
 /**
  * Wrapper component for using unstated
  */
-const PasswordResetModalWrapper = withUnstatedContainers(PasswordResetModalWrapperFC, [AppContainer]);
 
 PasswordResetModal.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 
   isOpen: PropTypes.bool.isRequired,
   onClose: PropTypes.func.isRequired,
@@ -129,4 +126,4 @@ PasswordResetModal.propTypes = {
 
 };
 
-export default PasswordResetModalWrapper;
+export default PasswordResetModalWrapperFC;

+ 3 - 5
packages/app/src/components/Admin/Users/SendInvitationEmailButton.jsx

@@ -1,10 +1,9 @@
 import React from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 import AdminUsersContainer from '~/client/services/AdminUsersContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Put } from '~/client/util/apiv3-client';
 
@@ -12,7 +11,7 @@ import { withUnstatedContainers } from '../../UnstatedUtils';
 
 const SendInvitationEmailButton = (props) => {
   const {
-    appContainer, user, isInvitationEmailSended, onSuccessfullySentInvitationEmail,
+    user, isInvitationEmailSended, onSuccessfullySentInvitationEmail,
   } = props;
   const { t } = useTranslation();
 
@@ -46,10 +45,9 @@ const SendInvitationEmailButton = (props) => {
   );
 };
 
-const SendInvitationEmailButtonWrapper = withUnstatedContainers(SendInvitationEmailButton, [AppContainer, AdminUsersContainer]);
+const SendInvitationEmailButtonWrapper = withUnstatedContainers(SendInvitationEmailButton, [AdminUsersContainer]);
 
 SendInvitationEmailButton.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   user: PropTypes.object.isRequired,
   isInvitationEmailSended: PropTypes.bool.isRequired,
   onSuccessfullySentInvitationEmail: PropTypes.func.isRequired,

+ 2 - 4
packages/app/src/components/Admin/Users/StatusActivateButton.jsx

@@ -1,10 +1,9 @@
 import React from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 import AdminUsersContainer from '~/client/services/AdminUsersContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -49,11 +48,10 @@ const StatusActivateFormWrapperFC = (props) => {
 /**
  * Wrapper component for using unstated
  */
-const StatusActivateFormWrapper = withUnstatedContainers(StatusActivateFormWrapperFC, [AppContainer, AdminUsersContainer]);
+const StatusActivateFormWrapper = withUnstatedContainers(StatusActivateFormWrapperFC, [AdminUsersContainer]);
 
 StatusActivateButton.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminUsersContainer: PropTypes.instanceOf(AdminUsersContainer).isRequired,
 
   user: PropTypes.object.isRequired,

+ 1 - 1
packages/app/src/components/Admin/Users/UserInviteModal.jsx

@@ -1,8 +1,8 @@
 import React from 'react';
 
+import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
-import { useTranslation } from 'next-i18next';
 // import Button from 'react-bootstrap/es/Button';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,

+ 2 - 4
packages/app/src/components/Admin/Users/UserMenu.jsx

@@ -1,13 +1,12 @@
 import React, { Fragment } from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 import {
   UncontrolledDropdown, DropdownToggle, DropdownMenu,
 } from 'reactstrap';
 
 import AdminUsersContainer from '~/client/services/AdminUsersContainer';
-import AppContainer from '~/client/services/AppContainer';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
@@ -121,11 +120,10 @@ const UserMenuWrapperFC = (props) => {
   return <UserMenu t={t} {...props} />;
 };
 
-const UserMenuWrapper = withUnstatedContainers(UserMenuWrapperFC, [AppContainer, AdminUsersContainer]);
+const UserMenuWrapper = withUnstatedContainers(UserMenuWrapperFC, [AdminUsersContainer]);
 
 UserMenu.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminUsersContainer: PropTypes.instanceOf(AdminUsersContainer).isRequired,
 
   user: PropTypes.object.isRequired,

+ 2 - 4
packages/app/src/components/Admin/Users/UserRemoveButton.jsx

@@ -1,10 +1,9 @@
 import React from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 
 import AdminUsersContainer from '~/client/services/AdminUsersContainer';
-import AppContainer from '~/client/services/AppContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -44,7 +43,6 @@ class UserRemoveButton extends React.Component {
 
 UserRemoveButton.propTypes = {
   t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminUsersContainer: PropTypes.instanceOf(AdminUsersContainer).isRequired,
 
   user: PropTypes.object.isRequired,
@@ -58,6 +56,6 @@ const UserRemoveButtonWrapperFC = (props) => {
 /**
  * Wrapper component for using unstated
  */
-const UserRemoveButtonWrapper = withUnstatedContainers(UserRemoveButtonWrapperFC, [AppContainer, AdminUsersContainer]);
+const UserRemoveButtonWrapper = withUnstatedContainers(UserRemoveButtonWrapperFC, [AdminUsersContainer]);
 
 export default UserRemoveButtonWrapper;

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

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

+ 18 - 0
packages/app/src/components/BookmarkButtons.module.scss

@@ -0,0 +1,18 @@
+@use '~/styles/bootstrap/init' as bs;
+
+.btn-bookmark {
+  :global {
+    box-shadow: none !important;
+  }
+
+  &:global {
+    @include bs.button-outline-variant(rgba(bs.$secondary, 50%), bs.$orange, rgba(lighten(bs.$orange, 20%), 0.5), rgba(lighten(bs.$orange, 20%), 0.5));
+    &:not(:disabled):not(.disabled):active,
+    &:not(:disabled):not(.disabled).active {
+      color: bs.$orange;
+    }
+    &:not(:disabled):not(.disabled):not(:hover) {
+      background-color: transparent;
+    }
+  }
+}

+ 9 - 2
packages/app/src/components/BookmarkButtons.tsx

@@ -9,6 +9,8 @@ import { IUser } from '../interfaces/user';
 
 import UserPictureList from './User/UserPictureList';
 
+import styles from './BookmarkButtons.module.scss';
+
 interface Props {
   bookmarkCount?: number
   isBookmarked?: boolean
@@ -55,7 +57,7 @@ const BookmarkButtons: FC<Props> = (props: Props) => {
         type="button"
         id="bookmark-button"
         onClick={handleClick}
-        className={`btn btn-bookmark border-0
+        className={`shadow-none btn ${styles['btn-bookmark']} border-0
           ${isBookmarked ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`}
       >
         <i className={`fa ${isBookmarked ? 'fa-bookmark' : 'fa-bookmark-o'}`}></i>
@@ -67,7 +69,12 @@ const BookmarkButtons: FC<Props> = (props: Props) => {
 
       { !hideTotalNumber && (
         <>
-          <button type="button" id="po-total-bookmarks" className={`btn btn-bookmark border-0 total-bookmarks ${props.isBookmarked ? 'active' : ''}`}>
+          <button
+            type="button"
+            id="po-total-bookmarks"
+            className={`shadow-none btn btn-bookmark border-0
+              total-bookmarks ${props.isBookmarked ? 'active' : ''}`}
+          >
             {bookmarkCount ?? 0}
           </button>
           { bookmarkedUsers != null && (

+ 2 - 1
packages/app/src/components/CompleteUserRegistrationForm.tsx

@@ -1,6 +1,7 @@
 import React, { useState, useEffect } from 'react';
 import { useTranslation } from 'next-i18next';
 import { apiv3Get, apiv3Post } from '~/client/util/apiv3-client';
+
 import { toastSuccess, toastError } from '../client/util/apiNotification';
 
 interface Props {
@@ -28,7 +29,7 @@ const CompleteUserRegistrationForm: React.FC<Props> = (props: Props) => {
   useEffect(() => {
     const delayDebounceFn = setTimeout(async() => {
       try {
-        const { data } = await apiv3Get('/check_username', { username });
+        const { data } = await apiv3Get('/check-username', { username });
         if (data.ok) {
           setUsernameAvailable(data.valid);
         }

+ 4 - 4
packages/app/src/components/DescendantsPageList.tsx

@@ -12,9 +12,9 @@ import { IPagingResult } from '~/interfaces/paging-result';
 import { OnDeletedFunction, OnPutBackedFunction } from '~/interfaces/ui';
 import { useIsGuestUser, useIsSharedUser, useIsTrashPage } from '~/stores/context';
 import {
-  useSWRxDescendantsPageListForCurrrentPath, useSWRxPageInfoForList, useSWRxPageList, useDescendantsPageListForCurrentPathTermManager,
-} from '~/stores/page';
-import { usePageTreeTermManager } from '~/stores/page-listing';
+  usePageTreeTermManager, useDescendantsPageListForCurrentPathTermManager, useSWRxDescendantsPageListForCurrrentPath,
+  useSWRxPageInfoForList, useSWRxPageList,
+} from '~/stores/page-listing';
 
 import { ForceHideMenuItems, MenuItemType } from './Common/Dropdown/PageItemControl';
 import PageList from './PageList/PageList';
@@ -45,7 +45,7 @@ export const DescendantsPageListSubstance = (props: SubstanceProps): JSX.Element
   const { data: isGuestUser } = useIsGuestUser();
 
   const pageIds = pagingResult?.items?.map(page => page._id);
-  const { injectTo } = useSWRxPageInfoForList(pageIds, true, true);
+  const { injectTo } = useSWRxPageInfoForList(pageIds, null, true, true);
 
   let pageWithMetas: IDataWithMeta<IPageHasId, IPageInfoForOperation>[] = [];
 

+ 1 - 0
packages/app/src/components/DescendantsPageListModal.tsx

@@ -77,6 +77,7 @@ export const DescendantsPageListModal = (): JSX.Element => {
       size="xl"
       isOpen={isOpened}
       toggle={close}
+      data-testid="page-accessories-modal"
       className={`grw-page-accessories-modal ${isWindowExpanded ? 'grw-modal-expanded' : ''} `}
     >
       <ModalHeader className="p-0" toggle={close} close={buttons}>

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

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

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