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

Merge branch 'master' into feat/110268-111686-get-questionnaire-order-periodically

Futa Arai 3 лет назад
Родитель
Сommit
cf1b63ea81
100 измененных файлов с 765 добавлено и 1391 удалено
  1. 0 0
      .github/dependabot.yml.org
  2. 2 2
      .github/workflows/release-rc.yml
  3. 2 2
      .github/workflows/release-slackbot-proxy.yml
  4. 9 8
      .github/workflows/release.yml
  5. 1 1
      lerna.json
  6. 2 1
      package.json
  7. 0 2
      packages/app/_obsolete/src/client/nologin.jsx
  8. 1 1
      packages/app/cypress.config.ts
  9. 15 30
      packages/app/package.json
  10. 3 29
      packages/app/public/static/locales/en_US/admin.json
  11. 3 4
      packages/app/public/static/locales/en_US/translation.json
  12. 3 29
      packages/app/public/static/locales/ja_JP/admin.json
  13. 3 3
      packages/app/public/static/locales/ja_JP/translation.json
  14. 3 29
      packages/app/public/static/locales/zh_CN/admin.json
  15. 3 4
      packages/app/public/static/locales/zh_CN/translation.json
  16. 0 77
      packages/app/src/client/services/AdminBasicSecurityContainer.js
  17. 8 12
      packages/app/src/client/services/AdminCustomizeContainer.js
  18. 0 18
      packages/app/src/client/services/AdminGeneralSecurityContainer.js
  19. 13 7
      packages/app/src/client/services/AdminMarkDownContainer.js
  20. 0 104
      packages/app/src/client/services/AdminTwitterSecurityContainer.js
  21. 32 0
      packages/app/src/client/services/layout.ts
  22. 8 1
      packages/app/src/client/services/page-operation.ts
  23. 3 2
      packages/app/src/components/Admin/App/SiteUrlSetting.tsx
  24. 8 8
      packages/app/src/components/Admin/Customize/Customize.jsx
  25. 4 1
      packages/app/src/components/Admin/Customize/CustomizeCssSetting.tsx
  26. 33 21
      packages/app/src/components/Admin/Customize/CustomizeNoscriptSetting.tsx
  27. 28 45
      packages/app/src/components/Admin/Customize/CustomizeScriptSetting.tsx
  28. 6 3
      packages/app/src/components/Admin/MarkdownSetting/XssForm.jsx
  29. 2 5
      packages/app/src/components/Admin/PluginsExtension/PluginInstallerForm.tsx
  30. 0 42
      packages/app/src/components/Admin/Security/BasicSecuritySetting.jsx
  31. 0 139
      packages/app/src/components/Admin/Security/BasicSecuritySettingContents.jsx
  32. 2 2
      packages/app/src/components/Admin/Security/FacebookSecuritySetting.jsx
  33. 10 28
      packages/app/src/components/Admin/Security/SecurityManagementContents.jsx
  34. 0 41
      packages/app/src/components/Admin/Security/TwitterSecuritySetting.jsx
  35. 0 223
      packages/app/src/components/Admin/Security/TwitterSecuritySettingContents.jsx
  36. 25 19
      packages/app/src/components/Common/Dropdown/PageItemControl.tsx
  37. 1 1
      packages/app/src/components/CompleteUserRegistration.tsx
  38. 1 1
      packages/app/src/components/DescendantsPageListModal.module.scss
  39. 2 2
      packages/app/src/components/DescendantsPageListModal.tsx
  40. 5 6
      packages/app/src/components/Layout/AdminLayout.tsx
  41. 15 12
      packages/app/src/components/Layout/BasicLayout.tsx
  42. 0 8
      packages/app/src/components/Layout/NoLoginLayout.module.scss
  43. 2 3
      packages/app/src/components/Layout/NoLoginLayout.tsx
  44. 1 3
      packages/app/src/components/Layout/RawLayout.tsx
  45. 2 11
      packages/app/src/components/Layout/SearchResultLayout.tsx
  46. 4 9
      packages/app/src/components/Layout/ShareLinkLayout.tsx
  47. 13 5
      packages/app/src/components/LoginForm.tsx
  48. 2 8
      packages/app/src/components/Me/AssociateModal.tsx
  49. 8 2
      packages/app/src/components/Navbar/AppearanceModeDropdown.tsx
  50. 13 15
      packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  51. 1 1
      packages/app/src/components/Navbar/GrowiNavbar.tsx
  52. 1 1
      packages/app/src/components/Navbar/PersonalDropdown.jsx
  53. 9 3
      packages/app/src/components/PageAlert/PageRedirectedAlert.tsx
  54. 10 5
      packages/app/src/components/PageAlert/TrashPageAlert.tsx
  55. 15 12
      packages/app/src/components/PageComment.tsx
  56. 1 1
      packages/app/src/components/PageEditor/Cheatsheet.tsx
  57. 3 3
      packages/app/src/components/PageEditor/Editor.tsx
  58. 20 33
      packages/app/src/components/PageEditor/LinkEditModal.jsx
  59. 0 25
      packages/app/src/components/PageEditor/OptionsSelector.tsx
  60. 9 5
      packages/app/src/components/PageEditorByHackmd.tsx
  61. 19 5
      packages/app/src/components/PageList/PageListItemL.tsx
  62. 3 1
      packages/app/src/components/PageStatusAlert.tsx
  63. 17 14
      packages/app/src/components/SearchPage.tsx
  64. 14 11
      packages/app/src/components/Sidebar.tsx
  65. 1 1
      packages/app/src/components/Sidebar/CustomSidebar.tsx
  66. 1 1
      packages/app/src/components/Sidebar/PageTree/PrivateLegacyPagesLink.tsx
  67. 1 1
      packages/app/src/components/Sidebar/RecentChanges.module.scss
  68. 4 4
      packages/app/src/components/Sidebar/SidebarContents.tsx
  69. 23 24
      packages/app/src/components/Sidebar/Skeleton/SidebarSkeleton.tsx
  70. 4 1
      packages/app/src/components/Sidebar/Skeleton/TagContentSkeleton.tsx
  71. 0 5
      packages/app/src/components/Sidebar/Tag.module.scss
  72. 2 3
      packages/app/src/components/StickyStretchableScroller.tsx
  73. 2 2
      packages/app/src/components/TableOfContents.tsx
  74. 1 1
      packages/app/src/components/User/UserInfo.tsx
  75. 3 27
      packages/app/src/interfaces/activity.ts
  76. 0 1
      packages/app/src/interfaces/editor-settings.ts
  77. 0 1
      packages/app/src/migrations/20180927102719-init-serverurl.js
  78. 25 0
      packages/app/src/migrations/20221219011829-remove-basic-auth-related-config.js
  79. 78 83
      packages/app/src/pages/[[...path]].page.tsx
  80. 13 3
      packages/app/src/pages/_app.page.tsx
  81. 34 10
      packages/app/src/pages/_document.page.tsx
  82. 8 6
      packages/app/src/pages/_private-legacy-pages.page.tsx
  83. 17 17
      packages/app/src/pages/_search.page.tsx
  84. 7 1
      packages/app/src/pages/admin/[...path].page.tsx
  85. 8 3
      packages/app/src/pages/admin/app.page.tsx
  86. 7 2
      packages/app/src/pages/admin/audit-log.page.tsx
  87. 8 3
      packages/app/src/pages/admin/customize.page.tsx
  88. 8 3
      packages/app/src/pages/admin/export.page.tsx
  89. 7 3
      packages/app/src/pages/admin/global-notification/[globalNotificationId].page.tsx
  90. 6 2
      packages/app/src/pages/admin/global-notification/new.page.tsx
  91. 8 3
      packages/app/src/pages/admin/importer.page.tsx
  92. 6 2
      packages/app/src/pages/admin/index.page.tsx
  93. 9 3
      packages/app/src/pages/admin/markdown.page.tsx
  94. 8 3
      packages/app/src/pages/admin/notification.page.tsx
  95. 6 2
      packages/app/src/pages/admin/plugins.page.tsx
  96. 7 2
      packages/app/src/pages/admin/search.page.tsx
  97. 8 11
      packages/app/src/pages/admin/security.page.tsx
  98. 7 2
      packages/app/src/pages/admin/slack-integration-legacy.page.tsx
  99. 8 3
      packages/app/src/pages/admin/slack-integration.page.tsx
  100. 7 3
      packages/app/src/pages/admin/user-group-detail/[userGroupId].page.tsx

+ 0 - 0
.github/dependabot.yml → .github/dependabot.yml.org


+ 2 - 2
.github/workflows/release-rc.yml

@@ -34,14 +34,14 @@ jobs:
         echo ${{ secrets. DOCKER_REGISTRY_PASSWORD }} | docker login --username wsmoogle --password-stdin
         echo ${{ secrets. DOCKER_REGISTRY_PASSWORD }} | docker login --username wsmoogle --password-stdin
 
 
     - name: Login to GitHub Container Registry
     - name: Login to GitHub Container Registry
-      uses: docker/login-action@v1
+      uses: docker/login-action@v2
       with:
       with:
         registry: ghcr.io
         registry: ghcr.io
         username: wsmoogle
         username: wsmoogle
         password: ${{ secrets.DOCKER_REGISTRY_ON_GITHUB_PASSWORD }}
         password: ${{ secrets.DOCKER_REGISTRY_ON_GITHUB_PASSWORD }}
 
 
     - name: Set up Docker Buildx
     - name: Set up Docker Buildx
-      uses: docker/setup-buildx-action@v1
+      uses: docker/setup-buildx-action@v2
 
 
     - name: Build and push
     - name: Build and push
       uses: docker/build-push-action@v2
       uses: docker/build-push-action@v2

+ 2 - 2
.github/workflows/release-slackbot-proxy.yml

@@ -36,7 +36,7 @@ jobs:
         echo ${{ secrets. DOCKER_REGISTRY_PASSWORD }} | docker login --username wsmoogle --password-stdin
         echo ${{ secrets. DOCKER_REGISTRY_PASSWORD }} | docker login --username wsmoogle --password-stdin
 
 
     - name: Login to GitHub Container Registry
     - name: Login to GitHub Container Registry
-      uses: docker/login-action@v1
+      uses: docker/login-action@v2
       with:
       with:
         registry: ghcr.io
         registry: ghcr.io
         username: wsmoogle
         username: wsmoogle
@@ -55,7 +55,7 @@ jobs:
         gcloud auth configure-docker --quiet
         gcloud auth configure-docker --quiet
 
 
     - name: Set up Docker Buildx
     - name: Set up Docker Buildx
-      uses: docker/setup-buildx-action@v1
+      uses: docker/setup-buildx-action@v2
 
 
     - name: Build and push
     - name: Build and push
       uses: docker/build-push-action@v2
       uses: docker/build-push-action@v2

+ 9 - 8
.github/workflows/release.yml

@@ -144,7 +144,7 @@ jobs:
 
 
     - name: Docker meta
     - name: Docker meta
       id: meta
       id: meta
-      uses: docker/metadata-action@v3
+      uses: docker/metadata-action@v4
       with:
       with:
         images: weseek/growi,ghcr.io/weseek/growi
         images: weseek/growi,ghcr.io/weseek/growi
         flavor: |
         flavor: |
@@ -155,22 +155,24 @@ jobs:
           type=semver,value=${{ needs.create-github-release.outputs.RELEASED_VERSION }},pattern={{major}}.{{minor}}
           type=semver,value=${{ needs.create-github-release.outputs.RELEASED_VERSION }},pattern={{major}}.{{minor}}
           type=semver,value=${{ needs.create-github-release.outputs.RELEASED_VERSION }},pattern={{major}}.{{minor}}.{{patch}}
           type=semver,value=${{ needs.create-github-release.outputs.RELEASED_VERSION }},pattern={{major}}.{{minor}}.{{patch}}
 
 
-    - name: Login to docker.io registry
-      run: |
-        echo ${{ secrets. DOCKER_REGISTRY_PASSWORD }} | docker login --username wsmoogle --password-stdin
+    - name: Login to Docker Hub
+      uses: docker/login-action@v2
+      with:
+        username: wsmoogle
+        password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
 
 
     - name: Login to GitHub Container Registry
     - name: Login to GitHub Container Registry
-      uses: docker/login-action@v1
+      uses: docker/login-action@v2
       with:
       with:
         registry: ghcr.io
         registry: ghcr.io
         username: wsmoogle
         username: wsmoogle
         password: ${{ secrets.DOCKER_REGISTRY_ON_GITHUB_PASSWORD }}
         password: ${{ secrets.DOCKER_REGISTRY_ON_GITHUB_PASSWORD }}
 
 
     - name: Set up Docker Buildx
     - name: Set up Docker Buildx
-      uses: docker/setup-buildx-action@v1
+      uses: docker/setup-buildx-action@v2
 
 
     - name: Build and push
     - name: Build and push
-      uses: docker/build-push-action@v2
+      uses: docker/build-push-action@v3
       with:
       with:
         context: .
         context: .
         file: ./packages/app/docker/Dockerfile
         file: ./packages/app/docker/Dockerfile
@@ -202,4 +204,3 @@ jobs:
       run: |
       run: |
         STATUS=`git status --porcelain`
         STATUS=`git status --porcelain`
         if [ -z "$STATUS" ]; then exit 0; else exit 1; fi
         if [ -z "$STATUS" ]; then exit 0; else exit 1; fi
-

+ 1 - 1
lerna.json

@@ -1,7 +1,7 @@
 {
 {
   "npmClient": "yarn",
   "npmClient": "yarn",
   "useWorkspaces": true,
   "useWorkspaces": true,
-  "version": "6.0.0-RC.9",
+  "version": "6.0.0-RC.13",
   "packages": [
   "packages": [
     "packages/*"
     "packages/*"
   ]
   ]

+ 2 - 1
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "growi",
   "name": "growi",
-  "version": "6.0.0-RC.9",
+  "version": "6.0.0-RC.13",
   "description": "Team collaboration software using markdown",
   "description": "Team collaboration software using markdown",
   "tags": [
   "tags": [
     "wiki",
     "wiki",
@@ -62,6 +62,7 @@
     "@typescript-eslint/eslint-plugin": "^5.0.0",
     "@typescript-eslint/eslint-plugin": "^5.0.0",
     "@typescript-eslint/parser": "^5.0.0",
     "@typescript-eslint/parser": "^5.0.0",
     "cypress": "^12.0.1",
     "cypress": "^12.0.1",
+    "cypress-wait-until": "^1.7.2",
     "eslint": "^8.18.0",
     "eslint": "^8.18.0",
     "eslint-config-next": "^12.1.6",
     "eslint-config-next": "^12.1.6",
     "eslint-config-weseek": "^2.1.0",
     "eslint-config-weseek": "^2.1.0",

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

@@ -49,10 +49,8 @@ if (loginFormElem) {
     google: loginFormElem.dataset.isGoogleAuthEnabled === 'true',
     google: loginFormElem.dataset.isGoogleAuthEnabled === 'true',
     github: loginFormElem.dataset.isGithubAuthEnabled === 'true',
     github: loginFormElem.dataset.isGithubAuthEnabled === 'true',
     facebook: loginFormElem.dataset.isFacebookAuthEnabled === 'true',
     facebook: loginFormElem.dataset.isFacebookAuthEnabled === 'true',
-    twitter: loginFormElem.dataset.isTwitterAuthEnabled === 'true',
     saml: loginFormElem.dataset.isSamlAuthEnabled === 'true',
     saml: loginFormElem.dataset.isSamlAuthEnabled === 'true',
     oidc: loginFormElem.dataset.isOidcAuthEnabled === 'true',
     oidc: loginFormElem.dataset.isOidcAuthEnabled === 'true',
-    basic: loginFormElem.dataset.isBasicAuthEnabled === 'true',
   };
   };
 
 
   Object.assign(componentMappings, {
   Object.assign(componentMappings, {

+ 1 - 1
packages/app/cypress.config.ts

@@ -16,6 +16,7 @@ export default defineConfig({
         return launchOptions;
         return launchOptions;
       });
       });
     },
     },
+    defaultCommandTimeout: 7000,
   },
   },
   fileServerFolder: 'test/cypress',
   fileServerFolder: 'test/cypress',
   fixturesFolder: 'test/cypress/fixtures',
   fixturesFolder: 'test/cypress/fixtures',
@@ -25,5 +26,4 @@ export default defineConfig({
   viewportWidth: 1400,
   viewportWidth: 1400,
   viewportHeight: 1024,
   viewportHeight: 1024,
 
 
-  defaultCommandTimeout: 30000,
 });
 });

+ 15 - 30
packages/app/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/app",
   "name": "@growi/app",
-  "version": "6.0.0-RC.9",
+  "version": "6.0.0-RC.13",
   "license": "MIT",
   "license": "MIT",
   "scripts": {
   "scripts": {
     "//// for production": "",
     "//// for production": "",
@@ -53,6 +53,7 @@
   "// comments for dependencies": {
   "// comments for dependencies": {
     "openid-client": "Node.js 12 or higher is required for openid-client@3 and above.",
     "openid-client": "Node.js 12 or higher is required for openid-client@3 and above.",
     "escape-string-regexp": "5.0.0 or above exports only ESM",
     "escape-string-regexp": "5.0.0 or above exports only ESM",
+    "next": "/Sandbox rendering is crashed with v12.3 or above ",
     "string-width": "5.0.0 or above exports only ESM."
     "string-width": "5.0.0 or above exports only ESM."
   },
   },
   "dependencies": {
   "dependencies": {
@@ -64,24 +65,22 @@
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@godaddy/terminus": "^4.9.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^6.0.0-RC.9",
-    "@growi/core": "^6.0.0-RC.9",
-    "@growi/hackmd": "^6.0.0-RC.9",
-    "@growi/preset-themes": "^6.0.0-RC.9",
-    "@growi/remark-drawio": "^6.0.0-RC.9",
-    "@growi/remark-growi-directive": "^6.0.0-RC.9",
-    "@growi/remark-lsx": "^6.0.0-RC.9",
-    "@growi/slack": "^6.0.0-RC.9",
+    "@growi/codemirror-textlint": "^6.0.0-RC.13",
+    "@growi/core": "^6.0.0-RC.13",
+    "@growi/hackmd": "^6.0.0-RC.13",
+    "@growi/preset-themes": "^6.0.0-RC.13",
+    "@growi/remark-drawio": "^6.0.0-RC.13",
+    "@growi/remark-growi-directive": "^6.0.0-RC.13",
+    "@growi/remark-lsx": "^6.0.0-RC.13",
+    "@growi/slack": "^6.0.0-RC.13",
     "@promster/express": "^7.0.2",
     "@promster/express": "^7.0.2",
     "@promster/server": "^7.0.4",
     "@promster/server": "^7.0.4",
-    "@slack/events-api": "^3.0.0",
     "@slack/web-api": "^6.2.4",
     "@slack/web-api": "^6.2.4",
     "@slack/webhook": "^6.0.0",
     "@slack/webhook": "^6.0.0",
     "JSONStream": "^1.3.5",
     "JSONStream": "^1.3.5",
     "archiver": "^5.3.0",
     "archiver": "^5.3.0",
     "array.prototype.flatmap": "^1.2.2",
     "array.prototype.flatmap": "^1.2.2",
     "async-canvas-to-blob": "^1.0.3",
     "async-canvas-to-blob": "^1.0.3",
-    "aws-sdk": "^2.1044.0",
     "axios": "^0.24.0",
     "axios": "^0.24.0",
     "axios-retry": "^3.2.4",
     "axios-retry": "^3.2.4",
     "body-parser": "^1.18.2",
     "body-parser": "^1.18.2",
@@ -134,7 +133,7 @@
     "mongoose-unique-validator": "^2.0.3",
     "mongoose-unique-validator": "^2.0.3",
     "multer": "~1.4.0",
     "multer": "~1.4.0",
     "multer-autoreap": "^1.0.3",
     "multer-autoreap": "^1.0.3",
-    "next": "^12.2.5",
+    "next": "~12.2",
     "next-i18next": "^12.1.0",
     "next-i18next": "^12.1.0",
     "next-superjson": "^0.0.4",
     "next-superjson": "^0.0.4",
     "next-themes": "^0.2.0",
     "next-themes": "^0.2.0",
@@ -147,13 +146,9 @@
     "passport": "^0.6.0",
     "passport": "^0.6.0",
     "passport-github": "^1.1.0",
     "passport-github": "^1.1.0",
     "passport-google-oauth20": "^2.0.0",
     "passport-google-oauth20": "^2.0.0",
-    "passport-http": "^0.3.0",
     "passport-ldapauth": "^3.0.1",
     "passport-ldapauth": "^3.0.1",
     "passport-local": "^1.0.0",
     "passport-local": "^1.0.0",
     "passport-saml": "^3.2.0",
     "passport-saml": "^3.2.0",
-    "passport-twitter": "^1.0.4",
-    "prism-themes": "^1.9.0",
-    "prom-client": "^13.0.0",
     "rate-limiter-flexible": "^2.3.7",
     "rate-limiter-flexible": "^2.3.7",
     "react": "^18.2.0",
     "react": "^18.2.0",
     "react-bootstrap-typeahead": "^5.2.2",
     "react-bootstrap-typeahead": "^5.2.2",
@@ -170,7 +165,7 @@
     "react-syntax-highlighter": "^15.5.0",
     "react-syntax-highlighter": "^15.5.0",
     "react-toastify": "^9.1.1",
     "react-toastify": "^9.1.1",
     "react-use-ripple": "^1.5.2",
     "react-use-ripple": "^1.5.2",
-    "reactstrap": "^8.9.0",
+    "reactstrap": "^8.10.1",
     "reconnecting-websocket": "^4.4.0",
     "reconnecting-websocket": "^4.4.0",
     "redis": "^3.0.2",
     "redis": "^3.0.2",
     "rehype-katex": "^6.0.2",
     "rehype-katex": "^6.0.2",
@@ -183,10 +178,7 @@
     "remark-gfm": "^3.0.1",
     "remark-gfm": "^3.0.1",
     "remark-math": "^5.1.1",
     "remark-math": "^5.1.1",
     "remark-wiki-link": "^1.0.4",
     "remark-wiki-link": "^1.0.4",
-    "rimraf": "^3.0.0",
-    "simplebar-react": "^2.3.6",
     "socket.io": "^4.2.0",
     "socket.io": "^4.2.0",
-    "sticky-events": "^3.4.11",
     "stream-to-promise": "^3.0.0",
     "stream-to-promise": "^3.0.0",
     "string-width": "=4.2.2",
     "string-width": "=4.2.2",
     "superjson": "^1.9.1",
     "superjson": "^1.9.1",
@@ -210,23 +202,16 @@
     "handsontable": "v7.0.0 or above is no loger MIT lisence."
     "handsontable": "v7.0.0 or above is no loger MIT lisence."
   },
   },
   "devDependencies": {
   "devDependencies": {
-    "@growi/ui": "^6.0.0-RC.9",
+    "@growi/ui": "^6.0.0-RC.13",
     "@handsontable/react": "=2.1.0",
     "@handsontable/react": "=2.1.0",
     "@icon/themify-icons": "1.0.1-alpha.3",
     "@icon/themify-icons": "1.0.1-alpha.3",
     "@next/bundle-analyzer": "^12.2.3",
     "@next/bundle-analyzer": "^12.2.3",
-    "@types/compression": "^1.7.0",
     "@types/express": "^4.17.11",
     "@types/express": "^4.17.11",
-    "@types/jquery": "^3.5.8",
-    "@types/multer": "^1.4.5",
     "@types/react-scroll": "^1.8.4",
     "@types/react-scroll": "^1.8.4",
     "autoprefixer": "^9.0.0",
     "autoprefixer": "^9.0.0",
     "babel-loader": "^8.2.5",
     "babel-loader": "^8.2.5",
     "bootstrap": "^4.6.1",
     "bootstrap": "^4.6.1",
-    "browser-sync": "^2.27.7",
-    "bunyan-debug": "^2.0.0",
-    "cli": "~1.0.1",
     "codemirror": "^5.64.0",
     "codemirror": "^5.64.0",
-    "colors": "=1.4.0",
     "connect-browser-sync": "^2.1.0",
     "connect-browser-sync": "^2.1.0",
     "core-js": "=2.6.9",
     "core-js": "=2.6.9",
     "diff2html": "^3.1.2",
     "diff2html": "^3.1.2",
@@ -244,23 +229,23 @@
     "material-icons": "^1.11.3",
     "material-icons": "^1.11.3",
     "morgan": "^1.10.0",
     "morgan": "^1.10.0",
     "next-transpile-modules": "^9.0.0",
     "next-transpile-modules": "^9.0.0",
-    "normalize-path": "^3.0.0",
     "penpal": "^4.0.0",
     "penpal": "^4.0.0",
     "plantuml-encoder": "^1.2.5",
     "plantuml-encoder": "^1.2.5",
     "prettier": "^1.19.1",
     "prettier": "^1.19.1",
     "react-codemirror2": "^6.0.0",
     "react-codemirror2": "^6.0.0",
     "react-copy-to-clipboard": "^5.0.1",
     "react-copy-to-clipboard": "^5.0.1",
     "react-dropzone": "^11.2.4",
     "react-dropzone": "^11.2.4",
-    "react-frame-component": "^4.0.0",
     "react-hotkeys": "^2.0.0",
     "react-hotkeys": "^2.0.0",
     "react-waypoint": "^10.1.0",
     "react-waypoint": "^10.1.0",
     "rehype-rewrite": "^3.0.6",
     "rehype-rewrite": "^3.0.6",
     "replacestream": "^4.0.3",
     "replacestream": "^4.0.3",
     "reveal.js": "^4.3.1",
     "reveal.js": "^4.3.1",
     "sass": "^1.53.0",
     "sass": "^1.53.0",
+    "simplebar-react": "^2.3.6",
     "simple-line-icons": "^2.5.5",
     "simple-line-icons": "^2.5.5",
     "simple-load-script": "^1.0.2",
     "simple-load-script": "^1.0.2",
     "socket.io-client": "^4.2.0",
     "socket.io-client": "^4.2.0",
+    "sticky-events": "^3.4.11",
     "swagger2openapi": "^5.3.1",
     "swagger2openapi": "^5.3.1",
     "ts-node-dev": "^2.0.0",
     "ts-node-dev": "^2.0.0",
     "tsc-alias": "^1.2.9"
     "tsc-alias": "^1.2.9"

+ 3 - 29
packages/app/public/static/locales/en_US/admin.json

@@ -157,13 +157,6 @@
       "attr_based_login_control_rule_example2": "<h5>Example for escaping</h5>If you would like to use URL as a query value, escape the following:<br><code>http&#92;:&#92;/&#92;/schemas.example.com&#92;/ws&#92;/2005&#92;/05&#92;/identity&#92;/claims&#92;/emailaddress: &quot;myname@example.com&quot;</code>",
       "attr_based_login_control_rule_example2": "<h5>Example for escaping</h5>If you would like to use URL as a query value, escape the following:<br><code>http&#92;:&#92;/&#92;/schemas.example.com&#92;/ws&#92;/2005&#92;/05&#92;/identity&#92;/claims&#92;/emailaddress: &quot;myname@example.com&quot;</code>",
       "updated_saml": "Succeeded to update SAML setting"
       "updated_saml": "Succeeded to update SAML setting"
     },
     },
-    "Basic": {
-      "enable_basic": "Enable Basic",
-      "name": "Basic Authentication",
-      "desc_1": "Login with <code>username</code> in Authorization header.",
-      "desc_2": "User will be automatically generated if not exist.",
-      "updated_basic": "Succeeded to update Basic setting"
-    },
     "OAuth": {
     "OAuth": {
       "enable_oidc": "Enable OIDC",
       "enable_oidc": "Enable OIDC",
       "register": "Register for %s",
       "register": "Register for %s",
@@ -181,16 +174,6 @@
       "Facebook": {
       "Facebook": {
         "name": "Facebook OAuth"
         "name": "Facebook OAuth"
       },
       },
-      "Twitter": {
-        "enable_twitter": "Enable Twitter OAuth",
-        "name": "Twitter OAuth",
-        "register_1": "Access {{link}}",
-        "register_2": "Sign in Twitter",
-        "register_3": "Create Credentials &rightarrow; OAuth client ID &rightarrow; Select \"Web application\"",
-        "register_4": "Register your OAuth App with one of Authorized redirect URIs as <code>{{url}}</code>",
-        "register_5": "Copy and paste your ClientID and Client Secret above",
-        "updated_twitter": "Succeeded to update Twitter OAuth setting"
-      },
       "GitHub": {
       "GitHub": {
         "enable_github": "Enable GitHub OAuth",
         "enable_github": "Enable GitHub OAuth",
         "name": "GitHub OAuth",
         "name": "GitHub OAuth",
@@ -214,7 +197,6 @@
       "how_to": {
       "how_to": {
         "google": "How to configure Google OAuth?",
         "google": "How to configure Google OAuth?",
         "github": "How to configure GitHub OAuth?",
         "github": "How to configure GitHub OAuth?",
-        "twitter": "How to configure Twitter OAuth?",
         "oidc": "How to configure OIDC?"
         "oidc": "How to configure OIDC?"
       }
       }
     },
     },
@@ -505,8 +487,8 @@
     "custom_title_detail_placeholder1": "<code>&#123;&#123;sitename&#125;&#125;</code> - The site name of this wiki.",
     "custom_title_detail_placeholder1": "<code>&#123;&#123;sitename&#125;&#125;</code> - The site name of this wiki.",
     "custom_title_detail_placeholder2": "<code>&#123;&#123;pagename&#125;&#125;</code> - The page name of the current page.",
     "custom_title_detail_placeholder2": "<code>&#123;&#123;pagename&#125;&#125;</code> - The page name of the current page.",
     "custom_title_detail_placeholder3": "<code>&#123;&#123;pagepath&#125;&#125;</code> - The page path of the current page.",
     "custom_title_detail_placeholder3": "<code>&#123;&#123;pagepath&#125;&#125;</code> - The page path of the current page.",
-    "custom_header": "Custom HTML header",
-    "custom_header_detail": "You can customize HTML header that applies all pages. Your custom script will be inserted in <code>&lt;header&gt;</code> but above other <code>&lt;script&gt;</code> tags.<br>Relaod page to see changes.",
+    "custom_noscript": "Custom Noscript",
+    "custom_noscript_detail": "You can customize Noscript code that applies all pages. Your custom Noscript will be inserted into the <code>&lt;noscript&gt;</code> tag that is located as the first element of body.<br>Relaod page to see changes.",
     "custom_css": "Custom CSS",
     "custom_css": "Custom CSS",
     "write_css": "You can write CSS that is applied to whole system.",
     "write_css": "You can write CSS that is applied to whole system.",
     "ctrl_space": "Ctrl+Space to autocomplete",
     "ctrl_space": "Ctrl+Space to autocomplete",
@@ -888,10 +870,8 @@
     "USER_LOGIN_WITH_LDAP": "Login with LDAP",
     "USER_LOGIN_WITH_LDAP": "Login with LDAP",
     "USER_LOGIN_WITH_GOOGLE": "Login with Google",
     "USER_LOGIN_WITH_GOOGLE": "Login with Google",
     "USER_LOGIN_WITH_GITHUB": "Login with GitHub",
     "USER_LOGIN_WITH_GITHUB": "Login with GitHub",
-    "USER_LOGIN_WITH_TWITTER": "Login with Twitter",
     "USER_LOGIN_WITH_OIDC": "Login with OIDC",
     "USER_LOGIN_WITH_OIDC": "Login with OIDC",
     "USER_LOGIN_WITH_SAML": "Login with SAML",
     "USER_LOGIN_WITH_SAML": "Login with SAML",
-    "USER_LOGIN_WITH_BASIC": "Login with BASIC",
     "USER_LOGIN_FAILURE": "Login failure",
     "USER_LOGIN_FAILURE": "Login failure",
     "USER_LOGOUT": "Logout",
     "USER_LOGOUT": "Logout",
     "USER_FOGOT_PASSWORD": "Request password reset",
     "USER_FOGOT_PASSWORD": "Request password reset",
@@ -969,18 +949,12 @@
     "ADMIN_AUTH_OIDC_ENABLED": "Enable OIDC auth",
     "ADMIN_AUTH_OIDC_ENABLED": "Enable OIDC auth",
     "ADMIN_AUTH_OIDC_DISABLED": "Disable OIDC auth",
     "ADMIN_AUTH_OIDC_DISABLED": "Disable OIDC auth",
     "ADMIN_AUTH_OIDC_UPDATE": "Update OIDC settings",
     "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_ENABLED": "Enable Google auth",
     "ADMIN_AUTH_GOOGLE_DISABLED": "Disable Google auth",
     "ADMIN_AUTH_GOOGLE_DISABLED": "Disable Google auth",
     "ADMIN_AUTH_GOOGLE_UPDATE": "Update Google auth settings",
     "ADMIN_AUTH_GOOGLE_UPDATE": "Update Google auth settings",
     "ADMIN_AUTH_GITHUB_ENABLED": "Enable GitHub auth",
     "ADMIN_AUTH_GITHUB_ENABLED": "Enable GitHub auth",
     "ADMIN_AUTH_GITHUB_DISABLED": "Disable GitHub auth",
     "ADMIN_AUTH_GITHUB_DISABLED": "Disable GitHub auth",
     "ADMIN_AUTH_GITHUB_UPDATE": "Update GitHub auth settings",
     "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_LINE_BREAK_UPDATE": "Update Link Break settings",
     "ADMIN_MARKDOWN_INDENT_UPDATE": "Update Indent settings",
     "ADMIN_MARKDOWN_INDENT_UPDATE": "Update Indent settings",
     "ADMIN_MARKDOWN_PRESENTATION_UPDATE": "Update Presentation setting",
     "ADMIN_MARKDOWN_PRESENTATION_UPDATE": "Update Presentation setting",
@@ -991,7 +965,7 @@
     "ADMIN_FUNCTION_UPDATE": "Update Function",
     "ADMIN_FUNCTION_UPDATE": "Update Function",
     "ADMIN_CODE_HIGHLIGHT_UPDATE": "Update Code Highlight",
     "ADMIN_CODE_HIGHLIGHT_UPDATE": "Update Code Highlight",
     "ADMIN_CUSTOM_TITLE_UPDATE": "Update Custom Title",
     "ADMIN_CUSTOM_TITLE_UPDATE": "Update Custom Title",
-    "ADMIN_CUSTOM_HTML_HEADER_UPDATE": "Update Custom HTML header",
+    "ADMIN_CUSTOM_NOSCRIPT_UPDATE": "Update Custom noscript",
     "ADMIN_CUSTOM_CSS_UPDATE": "Update Custom CSS",
     "ADMIN_CUSTOM_CSS_UPDATE": "Update Custom CSS",
     "ADMIN_CUSTOM_SCRIPT_UPDATE": "Update Custom script",
     "ADMIN_CUSTOM_SCRIPT_UPDATE": "Update Custom script",
     "ADMIN_ARCHIVE_DATA_UPLOAD": "Upload Archived Data",
     "ADMIN_ARCHIVE_DATA_UPLOAD": "Upload Archived Data",

+ 3 - 4
packages/app/public/static/locales/en_US/translation.json

@@ -114,7 +114,6 @@
   "external_account_management": "External Account Management",
   "external_account_management": "External Account Management",
   "UserGroup": "UserGroup",
   "UserGroup": "UserGroup",
   "Basic Settings": "Basic Settings",
   "Basic Settings": "Basic Settings",
-  "Basic authentication": "Basic authentication",
   "The contents entered here will be shown in the header etc": "The contents entered here will be shown in the header etc",
   "The contents entered here will be shown in the header etc": "The contents entered here will be shown in the header etc",
   "Public": "Public",
   "Public": "Public",
   "Anyone with the link": "Anyone with the link",
   "Anyone with the link": "Anyone with the link",
@@ -571,6 +570,7 @@
     }
     }
   },
   },
   "private_legacy_pages": {
   "private_legacy_pages": {
+    "title": "Private Legacy Pages",
     "bulk_operation": "Bulk operation",
     "bulk_operation": "Bulk operation",
     "convert_all_selected_pages": "Convert all to new v5 compatible format",
     "convert_all_selected_pages": "Convert all to new v5 compatible format",
     "input_path_to_convert": "Input a path to convert pages",
     "input_path_to_convert": "Input a path to convert pages",
@@ -605,8 +605,8 @@
     }
     }
   },
   },
   "login": {
   "login": {
-    "Sign in error": "Login error",
-    "Registration successful": "Registration successful. Please wait for administrator approval.",
+    "sign_in_error": "Login error",
+    "registration_successful": "registration_successful. Please wait for administrator approval.",
     "Setup": "Setup",
     "Setup": "Setup",
     "enabled_ldap_has_configuration_problem":"LDAP is enabled but the configuration has something wrong.",
     "enabled_ldap_has_configuration_problem":"LDAP is enabled but the configuration has something wrong.",
     "set_env_var_for_logs": "(Please set the environment variables <code>DEBUG=crowi:service:PassportService</code> to get the logs)"
     "set_env_var_for_logs": "(Please set the environment variables <code>DEBUG=crowi:service:PassportService</code> to get the logs)"
@@ -732,7 +732,6 @@
     "logout": "Logout"
     "logout": "Logout"
   },
   },
   "pagetree": {
   "pagetree": {
-    "private_legacy_pages": "Private Legacy Pages",
     "cannot_rename_a_title_that_contains_slash": "Cannot rename a title that contains '/'",
     "cannot_rename_a_title_that_contains_slash": "Cannot rename a title that contains '/'",
     "you_cannot_move_this_page_now": "You cannot move this page now",
     "you_cannot_move_this_page_now": "You cannot move this page now",
     "something_went_wrong_with_moving_page": "Something went wrong with moving page"
     "something_went_wrong_with_moving_page": "Something went wrong with moving page"

+ 3 - 29
packages/app/public/static/locales/ja_JP/admin.json

@@ -165,13 +165,6 @@
       "attr_based_login_control_rule_exampl2": "<h5>エスケープの例</h5>ルールに URL を利用したい場合は、次のようにエスケープしてください:<br><code>http&#92;:&#92;/&#92;/schemas.example.com&#92;/ws&#92;/2005&#92;/05&#92;/identity&#92;/claims&#92;/emailaddress: &quot;myname@example.com&quot;</code>",
       "attr_based_login_control_rule_exampl2": "<h5>エスケープの例</h5>ルールに URL を利用したい場合は、次のようにエスケープしてください:<br><code>http&#92;:&#92;/&#92;/schemas.example.com&#92;/ws&#92;/2005&#92;/05&#92;/identity&#92;/claims&#92;/emailaddress: &quot;myname@example.com&quot;</code>",
       "updated_saml": "Succeeded to update SAML setting"
       "updated_saml": "Succeeded to update SAML setting"
     },
     },
-    "Basic": {
-      "enable_basic": "Basic を有効にする",
-      "name": "Basic 認証",
-      "desc_1": "Authorization ヘッダに格納されている <code>username</code> でログインします。",
-      "desc_2": "ユーザーが存在しなかった場合は自動生成します。",
-      "updated_basic": "Basic認証 を更新しました"
-    },
     "OAuth": {
     "OAuth": {
       "enable_oidc": "OIDC を有効にする",
       "enable_oidc": "OIDC を有効にする",
       "register": "%sに登録",
       "register": "%sに登録",
@@ -189,16 +182,6 @@
       "Facebook": {
       "Facebook": {
         "name": "Facebook OAuth"
         "name": "Facebook OAuth"
       },
       },
-      "Twitter": {
-        "enable_twitter": "Twitter OAuth を有効にする",
-        "name": "Twitter OAuth",
-        "register_1": "{{link}} へアクセス",
-        "register_2": "Twitterにサインイン",
-        "register_3": "Create New Appをクリック &rightarrow; Application Detailsの各項目を入力",
-        "register_4": "Create your Twitter Applicationで作成",
-        "register_5": "上記フォームにクライアントIDとクライアントシークレットを入力",
-        "updated_twitter": "Twitter OAuth を更新しました"
-      },
       "GitHub": {
       "GitHub": {
         "enable_github": "GitHub OAuth を有効にする",
         "enable_github": "GitHub OAuth を有効にする",
         "name": "GitHub OAuth",
         "name": "GitHub OAuth",
@@ -222,7 +205,6 @@
       "how_to": {
       "how_to": {
         "google": "Google OAuth の設定方法",
         "google": "Google OAuth の設定方法",
         "github": "GitHub OAuth の設定方法",
         "github": "GitHub OAuth の設定方法",
-        "twitter": "Twitter OAuth の設定方法",
         "oidc": "OIDC の設定方法"
         "oidc": "OIDC の設定方法"
       }
       }
     },
     },
@@ -513,8 +495,8 @@
     "custom_title_detail_placeholder1": "<code>&#123;&#123;sitename&#125;&#125;</code> - この Wiki のサイト名",
     "custom_title_detail_placeholder1": "<code>&#123;&#123;sitename&#125;&#125;</code> - この Wiki のサイト名",
     "custom_title_detail_placeholder2": "<code>&#123;&#123;pagename&#125;&#125;</code> - 現在表示中のページ名",
     "custom_title_detail_placeholder2": "<code>&#123;&#123;pagename&#125;&#125;</code> - 現在表示中のページ名",
     "custom_title_detail_placeholder3": "<code>&#123;&#123;pagepath&#125;&#125;</code> - 現在表示中のページパス",
     "custom_title_detail_placeholder3": "<code>&#123;&#123;pagepath&#125;&#125;</code> - 現在表示中のページパス",
-    "custom_header": "カスタム HTML Header",
-    "custom_header_detail": "システム全体に適用される HTML を記述できます。<code>&lt;header&gt;</code> タグ内の他の <code>&lt;script&gt;</code> タグ読み込み前に展開されます。<br>変更の反映はページの更新が必要です。",
+    "custom_noscript": "カスタム Noscript",
+    "custom_noscript_detail": "システム全体に適用される HTML を記述できます。<code>&lt;body&gt;</code> タグ内の最初の <code>&lt;noscript&gt;</code> タグ内に展開されます。<br>変更の反映はページの更新が必要です。",
     "custom_css": "カスタム CSS",
     "custom_css": "カスタム CSS",
     "write_css": " システム全体に適用されるCSSを記述できます。",
     "write_css": " システム全体に適用されるCSSを記述できます。",
     "ctrl_space": "Ctrl+Space でコード補完",
     "ctrl_space": "Ctrl+Space でコード補完",
@@ -896,10 +878,8 @@
     "USER_LOGIN_WITH_LDAP": "LDAP 認証でログイン",
     "USER_LOGIN_WITH_LDAP": "LDAP 認証でログイン",
     "USER_LOGIN_WITH_GOOGLE": "Google 認証でログイン",
     "USER_LOGIN_WITH_GOOGLE": "Google 認証でログイン",
     "USER_LOGIN_WITH_GITHUB": "GitHub 認証でログイン",
     "USER_LOGIN_WITH_GITHUB": "GitHub 認証でログイン",
-    "USER_LOGIN_WITH_TWITTER": "Twitter 認証でログイン",
     "USER_LOGIN_WITH_OIDC": "OIDC 認証でログイン",
     "USER_LOGIN_WITH_OIDC": "OIDC 認証でログイン",
     "USER_LOGIN_WITH_SAML": "SAML 認証でログイン",
     "USER_LOGIN_WITH_SAML": "SAML 認証でログイン",
-    "USER_LOGIN_WITH_BASIC": "BASIC 認証でログイン",
     "USER_LOGIN_FAILURE": "ログイン失敗",
     "USER_LOGIN_FAILURE": "ログイン失敗",
     "USER_LOGOUT": "ログアウト",
     "USER_LOGOUT": "ログアウト",
     "USER_FOGOT_PASSWORD": "パスワードリセットのリクエスト",
     "USER_FOGOT_PASSWORD": "パスワードリセットのリクエスト",
@@ -977,18 +957,12 @@
     "ADMIN_AUTH_OIDC_ENABLED": "OIDC 認証を有効",
     "ADMIN_AUTH_OIDC_ENABLED": "OIDC 認証を有効",
     "ADMIN_AUTH_OIDC_DISABLED": "OIDC 認証を無効",
     "ADMIN_AUTH_OIDC_DISABLED": "OIDC 認証を無効",
     "ADMIN_AUTH_OIDC_UPDATE": "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_ENABLED": "Google 認証の有効",
     "ADMIN_AUTH_GOOGLE_DISABLED": "Google 認証の無効",
     "ADMIN_AUTH_GOOGLE_DISABLED": "Google 認証の無効",
     "ADMIN_AUTH_GOOGLE_UPDATE": "Google 認証設定の更新",
     "ADMIN_AUTH_GOOGLE_UPDATE": "Google 認証設定の更新",
     "ADMIN_AUTH_GITHUB_ENABLED": "GitHub 認証の有効",
     "ADMIN_AUTH_GITHUB_ENABLED": "GitHub 認証の有効",
     "ADMIN_AUTH_GITHUB_DISABLED": "GitHub 認証の無効",
     "ADMIN_AUTH_GITHUB_DISABLED": "GitHub 認証の無効",
     "ADMIN_AUTH_GITHUB_UPDATE": "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_LINE_BREAK_UPDATE": "Line Break 設定の更新",
     "ADMIN_MARKDOWN_INDENT_UPDATE": "インデント設定の更新",
     "ADMIN_MARKDOWN_INDENT_UPDATE": "インデント設定の更新",
     "ADMIN_MARKDOWN_PRESENTATION_UPDATE": "プレゼンテーション設定の更新",
     "ADMIN_MARKDOWN_PRESENTATION_UPDATE": "プレゼンテーション設定の更新",
@@ -999,7 +973,7 @@
     "ADMIN_FUNCTION_UPDATE": "機能設定の更新",
     "ADMIN_FUNCTION_UPDATE": "機能設定の更新",
     "ADMIN_CODE_HIGHLIGHT_UPDATE": "コードハイライト設定の更新",
     "ADMIN_CODE_HIGHLIGHT_UPDATE": "コードハイライト設定の更新",
     "ADMIN_CUSTOM_TITLE_UPDATE": "カスタムタイトル設定の更新",
     "ADMIN_CUSTOM_TITLE_UPDATE": "カスタムタイトル設定の更新",
-    "ADMIN_CUSTOM_HTML_HEADER_UPDATE": "カスタム HTML Header 設定の更新",
+    "ADMIN_CUSTOM_NOSCRIPT_UPDATE": "カスタム noscript 設定の更新",
     "ADMIN_CUSTOM_CSS_UPDATE": "カスタム CSS 設定の更新",
     "ADMIN_CUSTOM_CSS_UPDATE": "カスタム CSS 設定の更新",
     "ADMIN_CUSTOM_SCRIPT_UPDATE": "カスタムスクリプト設定の更新",
     "ADMIN_CUSTOM_SCRIPT_UPDATE": "カスタムスクリプト設定の更新",
     "ADMIN_ARCHIVE_DATA_UPLOAD": "アーカイブデータのアップロード",
     "ADMIN_ARCHIVE_DATA_UPLOAD": "アーカイブデータのアップロード",

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

@@ -569,6 +569,7 @@
     }
     }
   },
   },
   "private_legacy_pages": {
   "private_legacy_pages": {
+    "title": "旧形式のプライベートページ",
     "bulk_operation": "一括操作",
     "bulk_operation": "一括操作",
     "convert_all_selected_pages": "新しい v5 互換形式に一括変換",
     "convert_all_selected_pages": "新しい v5 互換形式に一括変換",
     "input_path_to_convert": "パスを入力して変換",
     "input_path_to_convert": "パスを入力して変換",
@@ -603,8 +604,8 @@
     }
     }
   },
   },
   "login": {
   "login": {
-    "Sign in error": "ログインエラー",
-    "Registration successful": "登録が完了しました。管理者の承認をお待ちください。",
+    "sign_in_error": "ログインエラー",
+    "registration_successful": "登録が完了しました。管理者の承認をお待ちください。",
     "Setup": "セットアップ",
     "Setup": "セットアップ",
     "enabled_ldap_has_configuration_problem":"LDAPは有効ですが、設定に問題があります。",
     "enabled_ldap_has_configuration_problem":"LDAPは有効ですが、設定に問題があります。",
     "set_env_var_for_logs": "(ログを取得するためには、環境変数 <code>DEBUG=crowi:service:PassportService</code> を設定してください。)"
     "set_env_var_for_logs": "(ログを取得するためには、環境変数 <code>DEBUG=crowi:service:PassportService</code> を設定してください。)"
@@ -730,7 +731,6 @@
     "logout": "ログアウト"
     "logout": "ログアウト"
   },
   },
   "pagetree": {
   "pagetree": {
-    "private_legacy_pages": "旧形式のプライベートページ",
     "cannot_rename_a_title_that_contains_slash": "`/` が含まれているタイトルにリネームできません",
     "cannot_rename_a_title_that_contains_slash": "`/` が含まれているタイトルにリネームできません",
     "you_cannot_move_this_page_now": "現在、このページを移動することはできません",
     "you_cannot_move_this_page_now": "現在、このページを移動することはできません",
     "something_went_wrong_with_moving_page": "ページの移動に問題が発生しました"
     "something_went_wrong_with_moving_page": "ページの移動に問題が発生しました"

+ 3 - 29
packages/app/public/static/locales/zh_CN/admin.json

@@ -165,13 +165,6 @@
       "attr_based_login_control_rule_example2": "<h5>Example for escaping</h5>If you would like to use URL as a query value, escape the following:<br><code>http&#92;:&#92;/&#92;/schemas.example.com&#92;/ws&#92;/2005&#92;/05&#92;/identity&#92;/claims&#92;/emailaddress: &quot;myname@example.com&quot;</code>",
       "attr_based_login_control_rule_example2": "<h5>Example for escaping</h5>If you would like to use URL as a query value, escape the following:<br><code>http&#92;:&#92;/&#92;/schemas.example.com&#92;/ws&#92;/2005&#92;/05&#92;/identity&#92;/claims&#92;/emailaddress: &quot;myname@example.com&quot;</code>",
       "updated_saml": "Succeeded to update SAML setting"
       "updated_saml": "Succeeded to update SAML setting"
 		},
 		},
-		"Basic": {
-			"enable_basic": "Enable Basic",
-			"name": "Basic Authentication",
-			"desc_1": "Login with <code>username</code> in Authorization header.",
-			"desc_2": "User will be automatically generated if not exist.",
-			"updated_basic": "Succeeded to update Basic setting"
-		},
 		"OAuth": {
 		"OAuth": {
 			"enable_oidc": "Enable OIDC",
 			"enable_oidc": "Enable OIDC",
 			"register": "Register for %s",
 			"register": "Register for %s",
@@ -189,16 +182,6 @@
 			"Facebook": {
 			"Facebook": {
 				"name": "Facebook OAuth"
 				"name": "Facebook OAuth"
 			},
 			},
-			"Twitter": {
-				"enable_twitter": "Enable Twitter OAuth",
-				"name": "Twitter OAuth",
-				"register_1": "Access {{link}}",
-				"register_2": "Sign in Twitter",
-				"register_3": "Create Credentials &rightarrow; OAuth client ID &rightarrow; Select \"Web application\"",
-				"register_4": "Register your OAuth App with one of Authorized redirect URIs as <code>{{url}}</code>",
-				"register_5": "Copy and paste your ClientID and Client Secret above",
-				"updated_twitter": "Succeeded to update Twitter OAuth setting"
-			},
 			"GitHub": {
 			"GitHub": {
 				"enable_github": "Enable GitHub OAuth",
 				"enable_github": "Enable GitHub OAuth",
 				"name": "GitHub OAuth",
 				"name": "GitHub OAuth",
@@ -222,7 +205,6 @@
 			"how_to": {
 			"how_to": {
 				"google": "How to configure Google OAuth?",
 				"google": "How to configure Google OAuth?",
 				"github": "How to configure GitHub OAuth?",
 				"github": "How to configure GitHub OAuth?",
-				"twitter": "How to configure Twitter OAuth?",
 				"oidc": "How to configure OIDC?"
 				"oidc": "How to configure OIDC?"
 			}
 			}
 		},
 		},
@@ -513,8 +495,8 @@
     "custom_title_detail_placeholder1": "<code>&#123;&#123;站点名称&#125;&#125;</code>-此wiki的站点名称。",
     "custom_title_detail_placeholder1": "<code>&#123;&#123;站点名称&#125;&#125;</code>-此wiki的站点名称。",
     "custom_title_detail_placeholder2": "<code>&#123;&#123;页名&#125;&#125;</code>-当前页的页名。",
     "custom_title_detail_placeholder2": "<code>&#123;&#123;页名&#125;&#125;</code>-当前页的页名。",
     "custom_title_detail_placeholder3": "<code>&#123;&#123;页面路径&#125;&#125;</code>-当前页面的页面路径。",
     "custom_title_detail_placeholder3": "<code>&#123;&#123;页面路径&#125;&#125;</code>-当前页面的页面路径。",
-    "custom_header": "自定义HTML标题",
-    "custom_header_detail": "您可以自定义应用所有页面的HTML标题。您的自定义脚本将插入<code>&lt;header&gt;</code>中,但位于其他<code>&lt;script&gt;</code>标记之上。<br>重新链接页面以查看更改。",
+    "custom_noscript": "自定义 Noscript 标题",
+    "custom_noscript_detail": "您可以自定义应用所有页面的 Noscript 代码。 您的自定义 Noscript 将被插入到作为 body 的第一个元素的 <code>&lt;noscript&gt;</code> 标签中。<br>重新链接页面以查看更改。",
     "custom_css": "自定义CSS",
     "custom_css": "自定义CSS",
     "write_css": "您可以编写应用于整个系统的CSS。",
     "write_css": "您可以编写应用于整个系统的CSS。",
     "ctrl_space": "Ctrl+Space 自动完成",
     "ctrl_space": "Ctrl+Space 自动完成",
@@ -896,10 +878,8 @@
     "USER_LOGIN_WITH_LDAP": "使用 LDAP 登录",
     "USER_LOGIN_WITH_LDAP": "使用 LDAP 登录",
     "USER_LOGIN_WITH_GOOGLE": "用谷歌登录",
     "USER_LOGIN_WITH_GOOGLE": "用谷歌登录",
     "USER_LOGIN_WITH_GITHUB": "使用 GitHub 登录",
     "USER_LOGIN_WITH_GITHUB": "使用 GitHub 登录",
-    "USER_LOGIN_WITH_TWITTER": "使用 Twitter 登录",
     "USER_LOGIN_WITH_OIDC": "使用 OIDC 登录",
     "USER_LOGIN_WITH_OIDC": "使用 OIDC 登录",
     "USER_LOGIN_WITH_SAML": "使用 SAML 登录",
     "USER_LOGIN_WITH_SAML": "使用 SAML 登录",
-    "USER_LOGIN_WITH_BASIC": "使用 BASIC 登录",
     "USER_LOGIN_FAILURE": "登录失败",
     "USER_LOGIN_FAILURE": "登录失败",
     "USER_LOGOUT": "注销",
     "USER_LOGOUT": "注销",
     "USER_FOGOT_PASSWORD": "要求重置密码",
     "USER_FOGOT_PASSWORD": "要求重置密码",
@@ -977,18 +957,12 @@
     "ADMIN_AUTH_OIDC_ENABLED": "启用 OIDC 身份验证",
     "ADMIN_AUTH_OIDC_ENABLED": "启用 OIDC 身份验证",
     "ADMIN_AUTH_OIDC_DISABLED": "禁用 OIDC 身份验证",
     "ADMIN_AUTH_OIDC_DISABLED": "禁用 OIDC 身份验证",
     "ADMIN_AUTH_OIDC_UPDATE": "更新 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_ENABLED": "启用谷歌身份验证",
     "ADMIN_AUTH_GOOGLE_DISABLED": "禁用谷歌身份验证",
     "ADMIN_AUTH_GOOGLE_DISABLED": "禁用谷歌身份验证",
     "ADMIN_AUTH_GOOGLE_UPDATE": "更新谷歌授权设置",
     "ADMIN_AUTH_GOOGLE_UPDATE": "更新谷歌授权设置",
     "ADMIN_AUTH_GITHUB_ENABLED": "启用 GitHub 身份验证",
     "ADMIN_AUTH_GITHUB_ENABLED": "启用 GitHub 身份验证",
     "ADMIN_AUTH_GITHUB_DISABLED": "禁用 GitHub 身份验证",
     "ADMIN_AUTH_GITHUB_DISABLED": "禁用 GitHub 身份验证",
     "ADMIN_AUTH_GITHUB_UPDATE": "更新 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_LINE_BREAK_UPDATE": "更新链接中断设置",
     "ADMIN_MARKDOWN_INDENT_UPDATE": "更新缩进设置",
     "ADMIN_MARKDOWN_INDENT_UPDATE": "更新缩进设置",
     "ADMIN_MARKDOWN_PRESENTATION_UPDATE": "更新演示设置",
     "ADMIN_MARKDOWN_PRESENTATION_UPDATE": "更新演示设置",
@@ -999,7 +973,7 @@
     "ADMIN_FUNCTION_UPDATE": "更新函数",
     "ADMIN_FUNCTION_UPDATE": "更新函数",
     "ADMIN_CODE_HIGHLIGHT_UPDATE": "更新代码高亮",
     "ADMIN_CODE_HIGHLIGHT_UPDATE": "更新代码高亮",
     "ADMIN_CUSTOM_TITLE_UPDATE": "更新自定义标题",
     "ADMIN_CUSTOM_TITLE_UPDATE": "更新自定义标题",
-    "ADMIN_CUSTOM_HTML_HEADER_UPDATE": "更新自定义 HTML 标头",
+    "ADMIN_CUSTOM_NOSCRIPT_UPDATE": "更新自定义 noscript 标头",
     "ADMIN_CUSTOM_CSS_UPDATE": "更新自定义 CSS",
     "ADMIN_CUSTOM_CSS_UPDATE": "更新自定义 CSS",
     "ADMIN_CUSTOM_SCRIPT_UPDATE": "更新自定义脚本",
     "ADMIN_CUSTOM_SCRIPT_UPDATE": "更新自定义脚本",
     "ADMIN_ARCHIVE_DATA_UPLOAD": "上传存档数据",
     "ADMIN_ARCHIVE_DATA_UPLOAD": "上传存档数据",

+ 3 - 4
packages/app/public/static/locales/zh_CN/translation.json

@@ -121,7 +121,6 @@
   "UserGroup": "用户组",
   "UserGroup": "用户组",
   "ChildUserGroup": "儿童用户组",
   "ChildUserGroup": "儿童用户组",
 	"Basic Settings": "基础设置",
 	"Basic Settings": "基础设置",
-	"Basic authentication": "基本身份验证",
 	"The contents entered here will be shown in the header etc": "此处输入的内容将显示在标题等中",
 	"The contents entered here will be shown in the header etc": "此处输入的内容将显示在标题等中",
 	"Public": "公共",
 	"Public": "公共",
 	"Anyone with the link": "任何人",
 	"Anyone with the link": "任何人",
@@ -575,6 +574,7 @@
     }
     }
 	},
 	},
   "private_legacy_pages": {
   "private_legacy_pages": {
+    "title": "私人遗留页面",
     "bulk_operation": "批量操作",
     "bulk_operation": "批量操作",
     "convert_all_selected_pages": "全部转换为新的v5兼容格式",
     "convert_all_selected_pages": "全部转换为新的v5兼容格式",
 		"input_path_to_convert": "输入一个转换页面的路径",
 		"input_path_to_convert": "输入一个转换页面的路径",
@@ -609,8 +609,8 @@
     }
     }
   },
   },
 	"login": {
 	"login": {
-		"Sign in error": "登录错误",
-		"Registration successful": "注册成功。请等待管理员批准",
+		"sign_in_error": "登录错误",
+		"registration_successful": "注册成功。请等待管理员批准",
 		"Setup": "安装程序",
 		"Setup": "安装程序",
     "enabled_ldap_has_configuration_problem":"启用了LDAP,但配置有问题。",
     "enabled_ldap_has_configuration_problem":"启用了LDAP,但配置有问题。",
     "set_env_var_for_logs": "(请设置环境变量 <code>DEBUG=crowi:service:PassportService</code> 以获得日志。)"
     "set_env_var_for_logs": "(请设置环境变量 <code>DEBUG=crowi:service:PassportService</code> 以获得日志。)"
@@ -736,7 +736,6 @@
     "logout": "登出"
     "logout": "登出"
   },
   },
   "pagetree": {
   "pagetree": {
-    "private_legacy_pages": "私人遗留页面",
     "cannot_rename_a_title_that_contains_slash": "不能重命名包含 ’/' 的标题",
     "cannot_rename_a_title_that_contains_slash": "不能重命名包含 ’/' 的标题",
     "you_cannot_move_this_page_now": "你现在不能移动这个页面",
     "you_cannot_move_this_page_now": "你现在不能移动这个页面",
     "something_went_wrong_with_moving_page": "移动页面时出了问题"
     "something_went_wrong_with_moving_page": "移动页面时出了问题"

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

@@ -1,77 +0,0 @@
-import { isServer } from '@growi/core';
-import { Container } from 'unstated';
-
-import loggerFactory from '~/utils/logger';
-import { removeNullPropertyFromObject } from '~/utils/object-utils';
-
-import { apiv3Get, apiv3Put } from '../util/apiv3-client';
-
-const logger = loggerFactory('growi:security:AdminTwitterSecurityContainer');
-
-/**
- * Service container for admin security page (BasicSecuritySetting.jsx)
- * @extends {Container} unstated Container
- */
-export default class AdminBasicSecurityContainer extends Container {
-
-  constructor() {
-    super();
-
-    if (isServer()) {
-      return;
-    }
-
-    this.state = {
-      isSameUsernameTreatedAsIdenticalUser: false,
-    };
-  }
-
-  /**
-   * retrieve security data
-   */
-  async retrieveSecurityData() {
-    try {
-      const response = await apiv3Get('/security-setting/');
-      const { basicAuth } = response.data.securityParams;
-      this.setState({
-        isSameUsernameTreatedAsIdenticalUser: basicAuth.isSameUsernameTreatedAsIdenticalUser,
-      });
-    }
-    catch (err) {
-      this.setState({ retrieveError: err });
-      logger.error(err);
-      throw new Error('Failed to fetch data');
-    }
-  }
-
-  /**
-   * Workaround for the mangling in production build to break constructor.name
-   */
-  static getClassName() {
-    return 'AdminBasicSecurityContainer';
-  }
-
-  /**
-   * Switch isSameUsernameTreatedAsIdenticalUser
-   */
-  switchIsSameUsernameTreatedAsIdenticalUser() {
-    this.setState({ isSameUsernameTreatedAsIdenticalUser: !this.state.isSameUsernameTreatedAsIdenticalUser });
-  }
-
-  /**
-   * Update basicSetting
-   */
-  async updateBasicSetting() {
-    let requestParams = { isSameUsernameTreatedAsIdenticalUser: this.state.isSameUsernameTreatedAsIdenticalUser };
-
-    requestParams = await removeNullPropertyFromObject(requestParams);
-    const response = await apiv3Put('/security-setting/basic', requestParams);
-    const { securitySettingParams } = response.data;
-
-    this.setState({
-      isSameUsernameTreatedAsIdenticalUser: securitySettingParams.isSameUsernameTreatedAsIdenticalUser,
-    });
-    return response;
-  }
-
-}

+ 8 - 12
packages/app/src/client/services/AdminCustomizeContainer.js

@@ -36,7 +36,7 @@ export default class AdminCustomizeContainer extends Container {
       isAllReplyShown: false,
       isAllReplyShown: false,
       isSearchScopeChildrenAsDefault: false,
       isSearchScopeChildrenAsDefault: false,
       currentCustomizeTitle: '',
       currentCustomizeTitle: '',
-      currentCustomizeHeader: '',
+      currentCustomizeNoscript: '',
       currentCustomizeCss: '',
       currentCustomizeCss: '',
       currentCustomizeScript: '',
       currentCustomizeScript: '',
     };
     };
@@ -73,7 +73,7 @@ export default class AdminCustomizeContainer extends Container {
         isAllReplyShown: customizeParams.isAllReplyShown,
         isAllReplyShown: customizeParams.isAllReplyShown,
         isSearchScopeChildrenAsDefault: customizeParams.isSearchScopeChildrenAsDefault,
         isSearchScopeChildrenAsDefault: customizeParams.isSearchScopeChildrenAsDefault,
         currentCustomizeTitle: customizeParams.customizeTitle,
         currentCustomizeTitle: customizeParams.customizeTitle,
-        currentCustomizeHeader: customizeParams.customizeHeader,
+        currentCustomizeNoscript: customizeParams.customizeNoscript,
         currentCustomizeCss: customizeParams.customizeCss,
         currentCustomizeCss: customizeParams.customizeCss,
         currentCustomizeScript: customizeParams.customizeScript,
         currentCustomizeScript: customizeParams.customizeScript,
       });
       });
@@ -160,8 +160,8 @@ export default class AdminCustomizeContainer extends Container {
   /**
   /**
    * Change customize Html header
    * Change customize Html header
    */
    */
-  changeCustomizeHeader(inputValue) {
-    this.setState({ currentCustomizeHeader: inputValue });
+  changeCustomizeNoscript(inputValue) {
+    this.setState({ currentCustomizeNoscript: inputValue });
   }
   }
 
 
   /**
   /**
@@ -235,18 +235,14 @@ export default class AdminCustomizeContainer extends Container {
     }
     }
   }
   }
 
 
-  /**
-   * Update customHeader
-   * @memberOf AdminCustomizeContainer
-   */
-  async updateCustomizeHeader() {
+  async updateCustomizeNoscript() {
     try {
     try {
-      const response = await apiv3Put('/customize-setting/customize-header', {
-        customizeHeader: this.state.currentCustomizeHeader,
+      const response = await apiv3Put('/customize-setting/customize-noscript', {
+        customizeNoscript: this.state.currentCustomizeNoscript,
       });
       });
       const { customizedParams } = response.data;
       const { customizedParams } = response.data;
       this.setState({
       this.setState({
-        currentCustomizeHeader: customizedParams.customizeHeader,
+        currentCustomizeNoscript: customizedParams.customizeNoscript,
       });
       });
     }
     }
     catch (err) {
     catch (err) {

+ 0 - 18
packages/app/src/client/services/AdminGeneralSecurityContainer.js

@@ -42,10 +42,8 @@ export default class AdminGeneralSecurityContainer extends Container {
       isLdapEnabled: false,
       isLdapEnabled: false,
       isSamlEnabled: false,
       isSamlEnabled: false,
       isOidcEnabled: false,
       isOidcEnabled: false,
-      isBasicEnabled: false,
       isGoogleEnabled: false,
       isGoogleEnabled: false,
       isGitHubEnabled: false,
       isGitHubEnabled: false,
-      isTwitterEnabled: false,
       setupStrategies: [],
       setupStrategies: [],
       disableLinkSharing: false,
       disableLinkSharing: false,
       shareLinks: [],
       shareLinks: [],
@@ -82,10 +80,8 @@ export default class AdminGeneralSecurityContainer extends Container {
       isLdapEnabled: generalAuth.isLdapEnabled,
       isLdapEnabled: generalAuth.isLdapEnabled,
       isSamlEnabled: generalAuth.isSamlEnabled,
       isSamlEnabled: generalAuth.isSamlEnabled,
       isOidcEnabled: generalAuth.isOidcEnabled,
       isOidcEnabled: generalAuth.isOidcEnabled,
-      isBasicEnabled: generalAuth.isBasicEnabled,
       isGoogleEnabled: generalAuth.isGoogleEnabled,
       isGoogleEnabled: generalAuth.isGoogleEnabled,
       isGitHubEnabled: generalAuth.isGitHubEnabled,
       isGitHubEnabled: generalAuth.isGitHubEnabled,
-      isTwitterEnabled: generalAuth.isTwitterEnabled,
     });
     });
   }
   }
 
 
@@ -318,13 +314,6 @@ export default class AdminGeneralSecurityContainer extends Container {
     this.switchAuthentication('isOidcEnabled', 'oidc');
     this.switchAuthentication('isOidcEnabled', 'oidc');
   }
   }
 
 
-  /**
-   * Switch Basic enabled
-   */
-  async switchIsBasicEnabled() {
-    this.switchAuthentication('isBasicEnabled', 'basic');
-  }
-
   /**
   /**
    * Switch GoogleOAuth enabled
    * Switch GoogleOAuth enabled
    */
    */
@@ -339,11 +328,4 @@ export default class AdminGeneralSecurityContainer extends Container {
     this.switchAuthentication('isGitHubEnabled', 'github');
     this.switchAuthentication('isGitHubEnabled', 'github');
   }
   }
 
 
-  /**
-   * Switch TwitterOAuth enabled
-   */
-  async switchIsTwitterOAuthEnabled() {
-    this.switchAuthentication('isTwitterEnabled', 'twitter');
-  }
-
 }
 }

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

@@ -29,7 +29,7 @@ export default class AdminMarkDownContainer extends Container {
       isEnabledXss: false,
       isEnabledXss: false,
       xssOption: '',
       xssOption: '',
       tagWhiteList: '',
       tagWhiteList: '',
-      attrWhiteList: '',
+      attrWhiteList: '{}',
     };
     };
 
 
     this.switchEnableXss = this.switchEnableXss.bind(this);
     this.switchEnableXss = this.switchEnableXss.bind(this);
@@ -119,19 +119,25 @@ export default class AdminMarkDownContainer extends Container {
    * Update Xss Setting
    * Update Xss Setting
    */
    */
   async updateXssSetting() {
   async updateXssSetting() {
-    let { tagWhiteList, attrWhiteList } = this.state;
+    let { tagWhiteList } = this.state;
+    const { attrWhiteList } = this.state;
 
 
     tagWhiteList = Array.isArray(tagWhiteList) ? tagWhiteList : tagWhiteList.split(',');
     tagWhiteList = Array.isArray(tagWhiteList) ? tagWhiteList : tagWhiteList.split(',');
-    attrWhiteList = Array.isArray(attrWhiteList) ? attrWhiteList : attrWhiteList.split(',');
 
 
-    const response = await apiv3Put('/markdown-setting/xss', {
+    try {
+      // Check if parsing is possible
+      JSON.parse(attrWhiteList);
+    }
+    catch (err) {
+      throw Error(err);
+    }
+
+    await apiv3Put('/markdown-setting/xss', {
       isEnabledXss: this.state.isEnabledXss,
       isEnabledXss: this.state.isEnabledXss,
       xssOption: this.state.xssOption,
       xssOption: this.state.xssOption,
       tagWhiteList,
       tagWhiteList,
-      attrWhiteList,
+      attrWhiteList: attrWhiteList ?? '{}',
     });
     });
-
-    return response;
   }
   }
 
 
   /**
   /**

+ 0 - 104
packages/app/src/client/services/AdminTwitterSecurityContainer.js

@@ -1,104 +0,0 @@
-import { isServer, pathUtils } from '@growi/core';
-import { Container } from 'unstated';
-import urljoin from 'url-join';
-
-import loggerFactory from '~/utils/logger';
-import { removeNullPropertyFromObject } from '~/utils/object-utils';
-
-import { apiv3Get, apiv3Put } from '../util/apiv3-client';
-
-const logger = loggerFactory('growi:security:AdminTwitterSecurityContainer');
-
-/**
- * Service container for admin security page (TwitterSecurityManagement.jsx)
- * @extends {Container} unstated Container
- */
-export default class AdminTwitterSecurityContainer extends Container {
-
-  constructor(appContainer) {
-    super();
-
-    if (isServer()) {
-      return;
-    }
-
-    this.appContainer = appContainer;
-
-    this.state = {
-      twitterConsumerKey: '',
-      twitterConsumerSecret: '',
-      isSameUsernameTreatedAsIdenticalUser: false,
-    };
-
-    this.updateTwitterSetting = this.updateTwitterSetting.bind(this);
-  }
-
-  /**
-   * retrieve security data
-   */
-  async retrieveSecurityData() {
-    try {
-      const response = await apiv3Get('/security-setting/');
-      const { twitterOAuth } = response.data.securityParams;
-      this.setState({
-        twitterConsumerKey: twitterOAuth.twitterConsumerKey,
-        twitterConsumerSecret: twitterOAuth.twitterConsumerSecret,
-        isSameUsernameTreatedAsIdenticalUser: twitterOAuth.isSameUsernameTreatedAsIdenticalUser,
-      });
-    }
-    catch (err) {
-      this.setState({ retrieveError: err });
-      logger.error(err);
-      throw new Error('Failed to fetch data');
-    }
-  }
-
-  /**
-   * Workaround for the mangling in production build to break constructor.name
-   */
-  static getClassName() {
-    return 'AdminTwitterSecurityContainer';
-  }
-
-  /**
-   * Change twitterConsumerKey
-   */
-  changeTwitterConsumerKey(value) {
-    this.setState({ twitterConsumerKey: value });
-  }
-
-  /**
-   * Change twitterConsumerSecret
-   */
-  changeTwitterConsumerSecret(value) {
-    this.setState({ twitterConsumerSecret: value });
-  }
-
-  /**
-   * Switch isSameUsernameTreatedAsIdenticalUser
-   */
-  switchIsSameUsernameTreatedAsIdenticalUser() {
-    this.setState({ isSameUsernameTreatedAsIdenticalUser: !this.state.isSameUsernameTreatedAsIdenticalUser });
-  }
-
-  /**
-   * Update twitterSetting
-   */
-  async updateTwitterSetting() {
-    const { twitterConsumerKey, twitterConsumerSecret, isSameUsernameTreatedAsIdenticalUser } = this.state;
-
-    let requestParams = { twitterConsumerKey, twitterConsumerSecret, isSameUsernameTreatedAsIdenticalUser };
-
-    requestParams = await removeNullPropertyFromObject(requestParams);
-    const response = await apiv3Put('/security-setting/twitter-oauth', requestParams);
-    const { securitySettingParams } = response.data;
-
-    this.setState({
-      twitterConsumerKey: securitySettingParams.twitterConsumerKey,
-      twitterConsumerSecret: securitySettingParams.twitterConsumerSecret,
-      isSameUsernameTreatedAsIdenticalUser: securitySettingParams.isSameUsernameTreatedAsIdenticalUser,
-    });
-    return response;
-  }
-
-}

+ 32 - 0
packages/app/src/client/services/layout.ts

@@ -0,0 +1,32 @@
+import { useIsContainerFluid, useShareLinkId } from '~/stores/context';
+import { useSWRxCurrentPage } from '~/stores/page';
+import { useEditorMode } from '~/stores/ui';
+
+export const useEditorModeClassName = (): string => {
+  const { getClassNamesByEditorMode } = useEditorMode();
+
+  // TODO: Enable `editing-sidebar` class somehow
+  // https://redmine.weseek.co.jp/issues/111527
+  // const classNames: string[] = [];
+  // if (currentPage != null) {
+  //   const isSidebar = currentPage.path === '/Sidebar';
+  //   classNames.push(...getClassNamesByEditorMode(/* isSidebar */));
+  // }
+
+  return `${getClassNamesByEditorMode().join(' ') ?? ''}`;
+};
+
+export const useCurrentGrowiLayoutFluidClassName = (): string => {
+  const { data: shareLinkId } = useShareLinkId();
+  const { data: currentPage } = useSWRxCurrentPage(shareLinkId ?? undefined);
+
+  const { data: dataIsContainerFluid } = useIsContainerFluid();
+
+  const isContainerFluidEachPage = currentPage == null || !('expandContentWidth' in currentPage)
+    ? null
+    : currentPage.expandContentWidth;
+  const isContainerFluidDefault = dataIsContainerFluid;
+  const isContainerFluid = isContainerFluidEachPage ?? isContainerFluidDefault;
+
+  return isContainerFluid ? 'growi-layout-fluid' : '';
+};

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

@@ -3,7 +3,7 @@ import urljoin from 'url-join';
 
 
 import { OptionsToSave } from '~/interfaces/page-operation';
 import { OptionsToSave } from '~/interfaces/page-operation';
 import { useCurrentPageId } from '~/stores/context';
 import { useCurrentPageId } from '~/stores/context';
-import { useIsEnabledUnsavedWarning, usePageTagsForEditors } from '~/stores/editor';
+import { useEditingMarkdown, useIsEnabledUnsavedWarning, usePageTagsForEditors } from '~/stores/editor';
 import { useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
 import { useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
 import { useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
 import { useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
@@ -181,6 +181,7 @@ export const useUpdateStateAfterSave = (pageId: string|undefined|null): (() => P
   const { setRemoteLatestPageData } = useSetRemoteLatestPageData();
   const { setRemoteLatestPageData } = useSetRemoteLatestPageData();
   const { mutate: mutateTagsInfo } = useSWRxTagsInfo(pageId);
   const { mutate: mutateTagsInfo } = useSWRxTagsInfo(pageId);
   const { sync: syncTagsInfoForEditor } = usePageTagsForEditors(pageId);
   const { sync: syncTagsInfoForEditor } = usePageTagsForEditors(pageId);
+  const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
 
 
   if (pageId == null) { return }
   if (pageId == null) { return }
 
 
@@ -194,6 +195,8 @@ export const useUpdateStateAfterSave = (pageId: string|undefined|null): (() => P
 
 
     if (updatedPage == null) { return }
     if (updatedPage == null) { return }
 
 
+    mutateEditingMarkdown(updatedPage.revision.body);
+
     const remoterevisionData = {
     const remoterevisionData = {
       remoteRevisionId: updatedPage.revision._id,
       remoteRevisionId: updatedPage.revision._id,
       remoteRevisionBody: updatedPage.revision.body,
       remoteRevisionBody: updatedPage.revision.body,
@@ -206,3 +209,7 @@ export const useUpdateStateAfterSave = (pageId: string|undefined|null): (() => P
     setRemoteLatestPageData(remoterevisionData);
     setRemoteLatestPageData(remoterevisionData);
   };
   };
 };
 };
+
+export const unlink = async(path: string): Promise<void> => {
+  await apiPost('/pages.unlink', { path });
+};

+ 3 - 2
packages/app/src/components/Admin/App/SiteUrlSetting.tsx

@@ -18,19 +18,20 @@ type Props = {
 
 
 const SiteUrlSetting = (props: Props) => {
 const SiteUrlSetting = (props: Props) => {
   const { t } = useTranslation('admin', { keyPrefix: 'app_setting' });
   const { t } = useTranslation('admin', { keyPrefix: 'app_setting' });
+  const { t: tCommon } = useTranslation('commons');
   const { adminAppContainer } = props;
   const { adminAppContainer } = props;
 
 
 
 
   const submitHandler = useCallback(async() => {
   const submitHandler = useCallback(async() => {
     try {
     try {
       await adminAppContainer.updateSiteUrlSettingHandler();
       await adminAppContainer.updateSiteUrlSettingHandler();
-      toastSuccess(t('toaster.update_successed', { target: t('site_url.title') }));
+      toastSuccess(tCommon('toaster.update_successed', { target: t('site_url.title') }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
       logger.error(err);
       logger.error(err);
     }
     }
-  }, [adminAppContainer, t]);
+  }, [adminAppContainer, t, tCommon]);
 
 
   return (
   return (
     <React.Fragment>
     <React.Fragment>

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

@@ -12,9 +12,9 @@ import { withUnstatedContainers } from '../../UnstatedUtils';
 
 
 import CustomizeCssSetting from './CustomizeCssSetting';
 import CustomizeCssSetting from './CustomizeCssSetting';
 import CustomizeFunctionSetting from './CustomizeFunctionSetting';
 import CustomizeFunctionSetting from './CustomizeFunctionSetting';
-import CustomizeHeaderSetting from './CustomizeHeaderSetting';
 import CustomizeLayoutSetting from './CustomizeLayoutSetting';
 import CustomizeLayoutSetting from './CustomizeLayoutSetting';
 import CustomizeLogoSetting from './CustomizeLogoSetting';
 import CustomizeLogoSetting from './CustomizeLogoSetting';
+import CustomizeNoscriptSetting from './CustomizeNoscriptSetting';
 import CustomizeScriptSetting from './CustomizeScriptSetting';
 import CustomizeScriptSetting from './CustomizeScriptSetting';
 import CustomizeSidebarSetting from './CustomizeSidebarSetting';
 import CustomizeSidebarSetting from './CustomizeSidebarSetting';
 import CustomizeThemeSetting from './CustomizeThemeSetting';
 import CustomizeThemeSetting from './CustomizeThemeSetting';
@@ -44,10 +44,13 @@ function Customize(props) {
   return (
   return (
     <div data-testid="admin-customize">
     <div data-testid="admin-customize">
       <div className="mb-5">
       <div className="mb-5">
-        <CustomizeLayoutSetting />
+        <CustomizeThemeSetting />
       </div>
       </div>
       <div className="mb-5">
       <div className="mb-5">
-        <CustomizeThemeSetting />
+        <CustomizeLogoSetting />
+      </div>
+      <div className="mb-5">
+        <CustomizeLayoutSetting />
       </div>
       </div>
       <div className="mb-5">
       <div className="mb-5">
         <CustomizeSidebarSetting />
         <CustomizeSidebarSetting />
@@ -59,16 +62,13 @@ function Customize(props) {
         <CustomizeTitle />
         <CustomizeTitle />
       </div>
       </div>
       <div className="mb-5">
       <div className="mb-5">
-        <CustomizeHeaderSetting />
+        <CustomizeScriptSetting />
       </div>
       </div>
       <div className="mb-5">
       <div className="mb-5">
         <CustomizeCssSetting />
         <CustomizeCssSetting />
       </div>
       </div>
       <div className="mb-5">
       <div className="mb-5">
-        <CustomizeScriptSetting />
-      </div>
-      <div className="mb-5">
-        <CustomizeLogoSetting />
+        <CustomizeNoscriptSetting />
       </div>
       </div>
     </div>
     </div>
   );
   );

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

@@ -45,13 +45,16 @@ const CustomizeCssSetting = (props: Props): JSX.Element => {
             <textarea
             <textarea
               className="form-control"
               className="form-control"
               name="customizeCss"
               name="customizeCss"
-              value={adminCustomizeContainer.state.currentCustomizeCss || ''}
+              rows={8}
+              defaultValue={adminCustomizeContainer.state.currentCustomizeCss || ''}
               onChange={(e) => { adminCustomizeContainer.changeCustomizeCss(e.target.value) }}
               onChange={(e) => { adminCustomizeContainer.changeCustomizeCss(e.target.value) }}
             />
             />
+            {/* disabled in v6.0.0 temporarily -- 2022.12.19 Yuki Takei
             <p className="form-text text-muted text-right">
             <p className="form-text text-muted text-right">
               <i className="fa fa-fw fa-keyboard-o" aria-hidden="true" />
               <i className="fa fa-fw fa-keyboard-o" aria-hidden="true" />
               {t('admin:customize_settings.ctrl_space')}
               {t('admin:customize_settings.ctrl_space')}
             </p>
             </p>
+            */}
           </div>
           </div>
 
 
           <AdminUpdateButtonRow onClick={onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
           <AdminUpdateButtonRow onClick={onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />

+ 33 - 21
packages/app/src/components/Admin/Customize/CustomizeHeaderSetting.tsx → packages/app/src/components/Admin/Customize/CustomizeNoscriptSetting.tsx

@@ -1,6 +1,8 @@
 import React, { useCallback } from 'react';
 import React, { useCallback } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
+import { PrismAsyncLight } from 'react-syntax-highlighter';
+import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
 import { Card, CardBody } from 'reactstrap';
 import { Card, CardBody } from 'reactstrap';
 
 
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
@@ -13,15 +15,15 @@ type Props = {
   adminCustomizeContainer: AdminCustomizeContainer
   adminCustomizeContainer: AdminCustomizeContainer
 }
 }
 
 
-const CustomizeHeaderSetting = (props: Props): JSX.Element => {
+const CustomizeNoscriptSetting = (props: Props): JSX.Element => {
 
 
   const { adminCustomizeContainer } = props;
   const { adminCustomizeContainer } = props;
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
   const onClickSubmit = useCallback(async() => {
   const onClickSubmit = useCallback(async() => {
     try {
     try {
-      await adminCustomizeContainer.updateCustomizeHeader();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_header'), ns: 'commons' }));
+      await adminCustomizeContainer.updateCustomizeNoscript();
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_noscript'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
@@ -32,38 +34,48 @@ const CustomizeHeaderSetting = (props: Props): JSX.Element => {
     <React.Fragment>
     <React.Fragment>
       <div className="row">
       <div className="row">
         <div className="col-12">
         <div className="col-12">
-          <h2 className="admin-setting-header">{t('admin:customize_settings.custom_header')}</h2>
+          <h2 className="admin-setting-header">{t('admin:customize_settings.custom_noscript')}</h2>
 
 
           <Card className="card well my-3">
           <Card className="card well my-3">
             <CardBody className="px-0 py-2">
             <CardBody className="px-0 py-2">
               <span
               <span
                 // eslint-disable-next-line react/no-danger
                 // eslint-disable-next-line react/no-danger
-                dangerouslySetInnerHTML={{ __html: t('admin:customize_settings.custom_header_detail') }}
+                dangerouslySetInnerHTML={{ __html: t('admin:customize_settings.custom_noscript_detail') }}
               />
               />
             </CardBody>
             </CardBody>
           </Card>
           </Card>
-          <div className="form-text text-muted">
-            { t('Example') }:
-            <pre>
-              {/* eslint-disable-next-line react/no-unescaped-entities */}
-              <code className="text-wrap">&lt;script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.13.0/build/languages/yaml.min.js"
-                defer&gt;&lt;/script&gt;
-              </code>
-            </pre>
-          </div>
 
 
           <div className="form-group">
           <div className="form-group">
             <textarea
             <textarea
               className="form-control"
               className="form-control"
-              name="customizeHeader"
-              value={adminCustomizeContainer.state.currentCustomizeHeader || ''}
-              onChange={(e) => { adminCustomizeContainer.changeCustomizeHeader(e.target.value) }}
+              name="customizeNoscript"
+              rows={8}
+              defaultValue={adminCustomizeContainer.state.currentCustomizeNoscript || ''}
+              onChange={(e) => { adminCustomizeContainer.changeCustomizeNoscript(e.target.value) }}
             />
             />
-            <p className="form-text text-muted text-right">
+            {/* disabled in v6.0.0 temporarily -- 2022.12.19 Yuki Takei
+            <span className="form-text text-muted text-right">
               <i className="fa fa-fw fa-keyboard-o" aria-hidden="true"></i>
               <i className="fa fa-fw fa-keyboard-o" aria-hidden="true"></i>
               {t('admin:customize_settings.ctrl_space')}
               {t('admin:customize_settings.ctrl_space')}
-            </p>
+            </span>
+            */}
+          </div>
+
+          <a className="text-muted"
+            data-toggle="collapse" href="#collapseExampleHtml" role="button" aria-expanded="false" aria-controls="collapseExampleHtml">
+            <i className="fa fa-fw fa-chevron-right" aria-hidden="true"></i>
+            Example for Google Tag Manager
+          </a>
+          <div className="collapse" id="collapseExampleHtml">
+            <PrismAsyncLight style={oneDark} language={'javascript'}
+            >
+              {`<iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXXX"
+  height="0"
+  width="0"
+  style="display:none;visibility:hidden"></iframe>`}
+            </PrismAsyncLight>
           </div>
           </div>
+
           <AdminUpdateButtonRow onClick={onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
           <AdminUpdateButtonRow onClick={onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
         </div>
         </div>
       </div>
       </div>
@@ -72,6 +84,6 @@ const CustomizeHeaderSetting = (props: Props): JSX.Element => {
 
 
 };
 };
 
 
-const CustomizeHeaderSettingWrapper = withUnstatedContainers(CustomizeHeaderSetting, [AdminCustomizeContainer]);
+const CustomizeNoscriptSettingWrapper = withUnstatedContainers(CustomizeNoscriptSetting, [AdminCustomizeContainer]);
 
 
-export default CustomizeHeaderSettingWrapper;
+export default CustomizeNoscriptSettingWrapper;

+ 28 - 45
packages/app/src/components/Admin/Customize/CustomizeScriptSetting.tsx

@@ -1,6 +1,8 @@
 import React, { useCallback } from 'react';
 import React, { useCallback } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
+import { PrismAsyncLight } from 'react-syntax-highlighter';
+import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
 import { Card, CardBody } from 'reactstrap';
 import { Card, CardBody } from 'reactstrap';
 
 
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
@@ -28,14 +30,6 @@ const CustomizeScriptSetting = (props: Props): JSX.Element => {
     }
     }
   }, [t, adminCustomizeContainer]);
   }, [t, adminCustomizeContainer]);
 
 
-  const getExampleCode = useCallback(() => {
-    return `console.log($('.main-container'));
-    window.addEventListener('load', (event) => {
-      console.log('config: ', appContainer.config);
-    });
-    `;
-  }, []);
-
   return (
   return (
     <React.Fragment>
     <React.Fragment>
       <div className="row">
       <div className="row">
@@ -48,51 +42,40 @@ const CustomizeScriptSetting = (props: Props): JSX.Element => {
             </CardBody>
             </CardBody>
           </Card>
           </Card>
 
 
-          <div className="form-text text-muted">
-            Placeholders:<br />
-            (Available after <code>load</code> event)
-          </div>
-          <table className="table table-borderless table-sm form-text text-muted offset-1 col-11">
-            <tbody>
-              <tr>
-                <th className="text-right"><code>$</code></th>
-                <td>jQuery instance</td>
-              </tr>
-              <tr>
-                <th className="text-right"><code>appContainer</code></th>
-                <td>GROWI App <a href="https://github.com/jamiebuilds/unstated">unstated container</a></td>
-              </tr>
-              <tr>
-                <th className="text-right"><code>growiRenderer</code></th>
-                <td>GROWI Renderer origin instance</td>
-              </tr>
-              <tr>
-                <th className="text-right"><code>growiPlugin</code></th>
-                <td>GROWI Plugin Manager instance</td>
-              </tr>
-              <tr>
-                <th className="text-right"><code>Crowi</code></th>
-                <td>Crowi legacy instance (jQuery based)</td>
-              </tr>
-            </tbody>
-          </table>
-
-          <div className="form-text text-muted">
-            Examples:
-            <pre><code className='language-javascript'>{getExampleCode()}</code></pre>
-          </div>
-
           <div className="form-group">
           <div className="form-group">
             <textarea
             <textarea
               className="form-control"
               className="form-control"
               name="customizeScript"
               name="customizeScript"
-              value={adminCustomizeContainer.state.currentCustomizeScript || ''}
+              rows={8}
+              defaultValue={adminCustomizeContainer.state.currentCustomizeScript || ''}
               onChange={(e) => { adminCustomizeContainer.changeCustomizeScript(e.target.value) }}
               onChange={(e) => { adminCustomizeContainer.changeCustomizeScript(e.target.value) }}
             />
             />
-            <p className="form-text text-muted text-right">
+            {/* disabled in v6.0.0 temporarily -- 2022.12.19 Yuki Takei
+            <span className="form-text text-muted text-right">
               <i className="fa fa-fw fa-keyboard-o" aria-hidden="true" />
               <i className="fa fa-fw fa-keyboard-o" aria-hidden="true" />
               {t('admin:customize_settings.ctrl_space')}
               {t('admin:customize_settings.ctrl_space')}
-            </p>
+            </span>
+            */}
+          </div>
+
+          <a className="text-muted"
+            data-toggle="collapse" href="#collapseExampleScript" role="button" aria-expanded="false" aria-controls="collapseExampleScript">
+            <i className="fa fa-fw fa-chevron-right" aria-hidden="true"></i>
+            Example for Google Tag Manager
+          </a>
+          <div className="collapse" id="collapseExampleScript">
+            <PrismAsyncLight style={oneDark} language={'javascript'}
+            >
+              {`(function(w,d,s,l,i){
+w[l]=w[l]||[];
+w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});
+var f=d.getElementsByTagName(s)[0],
+  j=d.createElement(s),
+  dl=l!='dataLayer'?'&l='+l:'';
+j.async=true;
+j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
+})(window,document,'script','dataLayer','GTM-XXXXXX');`}
+            </PrismAsyncLight>
           </div>
           </div>
 
 
           <AdminUpdateButtonRow onClick={onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
           <AdminUpdateButtonRow onClick={onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />

+ 6 - 3
packages/app/src/components/Admin/MarkdownSetting/XssForm.jsx

@@ -2,11 +2,11 @@ import React from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
+import { defaultSchema as sanitizeDefaultSchema } from 'rehype-sanitize';
 
 
 import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
 import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { RehypeSanitizeOption } from '~/interfaces/rehype';
 import { RehypeSanitizeOption } from '~/interfaces/rehype';
-import { tags, attrs } from '~/services/xss/recommended-whitelist';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -41,6 +41,9 @@ class XssForm extends React.Component {
     const { t, adminMarkDownContainer } = this.props;
     const { t, adminMarkDownContainer } = this.props;
     const { xssOption } = adminMarkDownContainer.state;
     const { xssOption } = adminMarkDownContainer.state;
 
 
+    const rehypeRecommendedTags = sanitizeDefaultSchema.tagNames;
+    const rehypeRecommendedAttributes = JSON.stringify(sanitizeDefaultSchema.attributes);
+
     return (
     return (
       <div className="form-group col-12 my-3">
       <div className="form-group col-12 my-3">
         <div className="row">
         <div className="row">
@@ -67,7 +70,7 @@ class XssForm extends React.Component {
                     rows="6"
                     rows="6"
                     cols="40"
                     cols="40"
                     readOnly
                     readOnly
-                    defaultValue={tags}
+                    defaultValue={rehypeRecommendedTags}
                   />
                   />
                 </div>
                 </div>
                 <div className="mt-4">
                 <div className="mt-4">
@@ -80,7 +83,7 @@ class XssForm extends React.Component {
                     rows="6"
                     rows="6"
                     cols="40"
                     cols="40"
                     readOnly
                     readOnly
-                    defaultValue={attrs}
+                    defaultValue={rehypeRecommendedAttributes}
                   />
                   />
                 </div>
                 </div>
               </label>
               </label>

+ 2 - 5
packages/app/src/components/Admin/PluginsExtension/PluginInstallerForm.tsx

@@ -3,9 +3,6 @@ import React, { useCallback } from 'react';
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 
 
-import AdminInstallButtonRow from '../Common/AdminUpdateButtonRow';
-// TODO: error notification (toast, loggerFactory)
-// TODO: i18n
 
 
 export const PluginInstallerForm = (): JSX.Element => {
 export const PluginInstallerForm = (): JSX.Element => {
   // const { t } = useTranslation('admin');
   // const { t } = useTranslation('admin');
@@ -31,8 +28,8 @@ export const PluginInstallerForm = (): JSX.Element => {
       await apiv3Post('/plugins', { pluginInstallerForm });
       await apiv3Post('/plugins', { pluginInstallerForm });
       toastSuccess('Plugin Install Successed!');
       toastSuccess('Plugin Install Successed!');
     }
     }
-    catch (err) {
-      toastError(err);
+    catch (e) {
+      toastError(e);
     }
     }
   }, []);
   }, []);
 
 

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

@@ -1,42 +0,0 @@
-import React, { useEffect, useCallback } from 'react';
-
-import PropTypes from 'prop-types';
-
-import AdminBasicSecurityContainer from '~/client/services/AdminBasicSecurityContainer';
-import { toastError } from '~/client/util/apiNotification';
-import { toArrayIfNot } from '~/utils/array-utils';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-
-import BasicSecurityManagementContents from './BasicSecuritySettingContents';
-
-const BasicSecurityManagement = (props) => {
-  const { adminBasicSecurityContainer } = props;
-
-  const fetchBasicSecuritySettingsData = useCallback(async() => {
-    try {
-      await adminBasicSecurityContainer.retrieveSecurityData();
-    }
-    catch (err) {
-      const errs = toArrayIfNot(err);
-      toastError(errs);
-    }
-  }, [adminBasicSecurityContainer]);
-
-  useEffect(() => {
-    fetchBasicSecuritySettingsData();
-  }, [adminBasicSecurityContainer, fetchBasicSecuritySettingsData]);
-
-
-  return <BasicSecurityManagementContents />;
-};
-
-BasicSecurityManagement.propTypes = {
-  adminBasicSecurityContainer: PropTypes.instanceOf(AdminBasicSecurityContainer).isRequired,
-};
-
-const BasicSecurityManagementWithUnstatedContainer = withUnstatedContainers(BasicSecurityManagement, [
-  AdminBasicSecurityContainer,
-]);
-
-export default BasicSecurityManagementWithUnstatedContainer;

+ 0 - 139
packages/app/src/components/Admin/Security/BasicSecuritySettingContents.jsx

@@ -1,139 +0,0 @@
-/* eslint-disable react/no-danger */
-import React from 'react';
-
-import PropTypes from 'prop-types';
-import { useTranslation } from 'next-i18next';
-
-import AdminBasicSecurityContainer from '~/client/services/AdminBasicSecurityContainer';
-import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-
-class BasicSecurityManagementContents extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.onClickSubmit = this.onClickSubmit.bind(this);
-  }
-
-  async onClickSubmit() {
-    const { t, adminBasicSecurityContainer, adminGeneralSecurityContainer } = this.props;
-
-    try {
-      await adminBasicSecurityContainer.updateBasicSetting();
-      await adminGeneralSecurityContainer.retrieveSetupStratedies();
-      toastSuccess(t('security_settings.Basic.updated_basic'));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }
-
-  render() {
-    const { t, adminGeneralSecurityContainer, adminBasicSecurityContainer } = this.props;
-    const { isBasicEnabled } = adminGeneralSecurityContainer.state;
-
-    return (
-      <React.Fragment>
-
-        <h2 className="alert-anchor border-bottom">
-          { t('security_settings.Basic.name') }
-        </h2>
-
-        {adminBasicSecurityContainer.state.retrieveError != null && (
-          <div className="alert alert-danger">
-            <p>{t('Error occurred')} : {adminBasicSecurityContainer.state.retrieveError}</p>
-          </div>
-        )}
-
-        <div className="form-group row">
-          <div className="col-6 offset-3">
-            <div className="custom-control custom-switch custom-checkbox-success">
-              <input
-                id="isBasicEnabled"
-                className="custom-control-input"
-                type="checkbox"
-                checked={adminGeneralSecurityContainer.state.isBasicEnabled}
-                onChange={() => { adminGeneralSecurityContainer.switchIsBasicEnabled() }}
-              />
-              <label className="custom-control-label" htmlFor="isBasicEnabled">
-                { t('security_settings.Basic.enable_basic') }
-              </label>
-            </div>
-            <p className="form-text text-muted">
-              <small>
-                <span dangerouslySetInnerHTML={{ __html: t('security_settings.Basic.desc_1') }} /><br />
-                { t('security_settings.Basic.desc_2')}
-              </small>
-            </p>
-            {(!adminGeneralSecurityContainer.state.setupStrategies.includes('basic') && isBasicEnabled)
-            && <div className="badge badge-warning">{t('security_settings.setup_is_not_yet_complete')}</div>}
-          </div>
-        </div>
-
-        {isBasicEnabled && (
-          <React.Fragment>
-            <div className="row mb-5">
-              <div className="offset-md-3 col-md-6">
-                <div className="custom-control custom-checkbox custom-checkbox-success">
-                  <input
-                    id="bindByEmail-basic"
-                    className="custom-control-input"
-                    type="checkbox"
-                    checked={adminBasicSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser || false}
-                    onChange={() => { adminBasicSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
-                  />
-                  <label
-                    className="custom-control-label"
-                    htmlFor="bindByEmail-basic"
-                    dangerouslySetInnerHTML={{ __html: t('security_settings.Treat username matching as identical', 'username') }}
-                  />
-                </div>
-                <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Treat username matching as identical_warn', 'username') }} />
-                </p>
-              </div>
-            </div>
-
-            <div className="row my-3">
-              <div className="offset-4 col-5">
-                <button
-                  type="button"
-                  className="btn btn-primary"
-                  disabled={adminBasicSecurityContainer.state.retrieveError != null}
-                  onClick={this.onClickSubmit}
-                >
-                  {t('Update')}
-                </button>
-              </div>
-            </div>
-
-          </React.Fragment>
-        )}
-
-      </React.Fragment>
-    );
-  }
-
-}
-
-BasicSecurityManagementContents.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
-  adminBasicSecurityContainer: PropTypes.instanceOf(AdminBasicSecurityContainer).isRequired,
-};
-
-const BasicSecurityManagementContentsWrapperFC = (props) => {
-  const { t } = useTranslation('admin');
-
-  return <BasicSecurityManagementContents t={t} {...props} />;
-};
-
-const BasicSecurityManagementContentsWrapper = withUnstatedContainers(BasicSecurityManagementContentsWrapperFC, [
-  AdminGeneralSecurityContainer,
-  AdminBasicSecurityContainer,
-]);
-
-export default BasicSecurityManagementContentsWrapper;

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

@@ -31,6 +31,6 @@ FacebookSecurityManagement.propTypes = {
   adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
   adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
 };
 };
 
 
-const TwitterSecurityManagementWrapper = withUnstatedContainers(FacebookSecurityManagement, [AdminGeneralSecurityContainer]);
+const FacebookSecurityManagementWrapper = withUnstatedContainers(FacebookSecurityManagement, [AdminGeneralSecurityContainer]);
 
 
-export default withTranslation()(TwitterSecurityManagementWrapper);
+export default withTranslation()(FacebookSecurityManagementWrapper);

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

@@ -6,8 +6,7 @@ import { TabContent, TabPane } from 'reactstrap';
 
 
 import CustomNav from '../../CustomNavigation/CustomNav';
 import CustomNav from '../../CustomNavigation/CustomNav';
 
 
-import BasicSecuritySetting from './BasicSecuritySetting';
-import FacebookSecuritySetting from './FacebookSecuritySetting';
+// import FacebookSecuritySetting from './FacebookSecuritySetting';
 import GitHubSecuritySetting from './GitHubSecuritySetting';
 import GitHubSecuritySetting from './GitHubSecuritySetting';
 import GoogleSecuritySetting from './GoogleSecuritySetting';
 import GoogleSecuritySetting from './GoogleSecuritySetting';
 import LdapSecuritySetting from './LdapSecuritySetting';
 import LdapSecuritySetting from './LdapSecuritySetting';
@@ -16,7 +15,6 @@ import OidcSecuritySetting from './OidcSecuritySetting';
 import SamlSecuritySetting from './SamlSecuritySetting';
 import SamlSecuritySetting from './SamlSecuritySetting';
 import SecuritySetting from './SecuritySetting';
 import SecuritySetting from './SecuritySetting';
 import ShareLinkSetting from './ShareLinkSetting';
 import ShareLinkSetting from './ShareLinkSetting';
-import TwitterSecuritySetting from './TwitterSecuritySetting';
 
 
 const SecurityManagementContents = () => {
 const SecurityManagementContents = () => {
   const { t } = useTranslation('admin');
   const { t } = useTranslation('admin');
@@ -51,31 +49,21 @@ const SecurityManagementContents = () => {
         i18n: 'OIDC',
         i18n: 'OIDC',
         index: 3,
         index: 3,
       },
       },
-      passport_basic: {
-        Icon: () => <i className="fa fa-lock" />,
-        i18n: 'BASIC',
-        index: 4,
-      },
       passport_google: {
       passport_google: {
         Icon: () => <i className="fa fa-google" />,
         Icon: () => <i className="fa fa-google" />,
         i18n: 'Google',
         i18n: 'Google',
-        index: 5,
+        index: 4,
       },
       },
       passport_github: {
       passport_github: {
         Icon: () => <i className="fa fa-github" />,
         Icon: () => <i className="fa fa-github" />,
         i18n: 'GitHub',
         i18n: 'GitHub',
-        index: 6,
-      },
-      passport_twitter: {
-        Icon: () => <i className="fa fa-twitter" />,
-        i18n: 'Twitter',
-        index: 7,
-      },
-      passport_facebook: {
-        Icon: () => <i className="fa fa-facebook" />,
-        i18n: '(TBD) Facebook',
-        index: 8,
+        index: 5,
       },
       },
+      // passport_facebook: {
+      //   Icon: () => <i className="fa fa-facebook" />,
+      //   i18n: '(TBD) Facebook',
+      //   index: 7,
+      // },
     };
     };
   }, []);
   }, []);
 
 
@@ -126,21 +114,15 @@ const SecurityManagementContents = () => {
           <TabPane tabId="passport_oidc">
           <TabPane tabId="passport_oidc">
             {activeComponents.has('passport_oidc') && <OidcSecuritySetting />}
             {activeComponents.has('passport_oidc') && <OidcSecuritySetting />}
           </TabPane>
           </TabPane>
-          <TabPane tabId="passport_basic">
-            {activeComponents.has('passport_basic') && <BasicSecuritySetting />}
-          </TabPane>
           <TabPane tabId="passport_google">
           <TabPane tabId="passport_google">
             {activeComponents.has('passport_google') && <GoogleSecuritySetting />}
             {activeComponents.has('passport_google') && <GoogleSecuritySetting />}
           </TabPane>
           </TabPane>
           <TabPane tabId="passport_github">
           <TabPane tabId="passport_github">
             {activeComponents.has('passport_github') && <GitHubSecuritySetting />}
             {activeComponents.has('passport_github') && <GitHubSecuritySetting />}
           </TabPane>
           </TabPane>
-          <TabPane tabId="passport_twitter">
-            {activeComponents.has('passport_twitter') && <TwitterSecuritySetting />}
-          </TabPane>
-          <TabPane tabId="passport_facebook">
+          {/* <TabPane tabId="passport_facebook">
             {activeComponents.has('passport_facebook') && <FacebookSecuritySetting />}
             {activeComponents.has('passport_facebook') && <FacebookSecuritySetting />}
-          </TabPane>
+          </TabPane> */}
         </TabContent>
         </TabContent>
       </div>
       </div>
     </div>
     </div>

+ 0 - 41
packages/app/src/components/Admin/Security/TwitterSecuritySetting.jsx

@@ -1,41 +0,0 @@
-import React, { useEffect, useCallback } from 'react';
-
-import PropTypes from 'prop-types';
-
-import AdminTwitterSecurityContainer from '~/client/services/AdminTwitterSecurityContainer';
-import { toastError } from '~/client/util/apiNotification';
-import { toArrayIfNot } from '~/utils/array-utils';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-
-import TwitterSecuritySettingContents from './TwitterSecuritySettingContents';
-
-const TwitterSecurityManagement = (props) => {
-  const { adminTwitterSecurityContainer } = props;
-
-  const fetchTwitterSecuritySettingsData = useCallback(async() => {
-    try {
-      await adminTwitterSecurityContainer.retrieveSecurityData();
-    }
-    catch (err) {
-      const errs = toArrayIfNot(err);
-      toastError(errs);
-    }
-  }, [adminTwitterSecurityContainer]);
-
-  useEffect(() => {
-    fetchTwitterSecuritySettingsData();
-  }, [adminTwitterSecurityContainer, fetchTwitterSecuritySettingsData]);
-
-  return <TwitterSecuritySettingContents />;
-};
-
-TwitterSecurityManagement.propTypes = {
-  adminTwitterSecurityContainer: PropTypes.instanceOf(AdminTwitterSecurityContainer).isRequired,
-};
-
-const TwitterSecurityManagementWithUnstatedContainer = withUnstatedContainers(TwitterSecurityManagement, [
-  AdminTwitterSecurityContainer,
-]);
-
-export default TwitterSecurityManagementWithUnstatedContainer;

+ 0 - 223
packages/app/src/components/Admin/Security/TwitterSecuritySettingContents.jsx

@@ -1,223 +0,0 @@
-/* eslint-disable react/no-danger */
-import React from 'react';
-
-import { pathUtils } from '@growi/core';
-import { useTranslation } from 'next-i18next';
-import PropTypes from 'prop-types';
-import urljoin from 'url-join';
-
-
-import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
-import AdminTwitterSecurityContainer from '~/client/services/AdminTwitterSecurityContainer';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
-import { useSiteUrl } from '~/stores/context';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-
-class TwitterSecuritySettingContents extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.onClickSubmit = this.onClickSubmit.bind(this);
-  }
-
-  async onClickSubmit() {
-    const { t, adminTwitterSecurityContainer, adminGeneralSecurityContainer } = this.props;
-
-    try {
-      await adminTwitterSecurityContainer.updateTwitterSetting();
-      await adminGeneralSecurityContainer.retrieveSetupStratedies();
-      toastSuccess(t('security_settings.OAuth.Twitter.updated_twitter'));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }
-
-  render() {
-    const {
-      t, adminGeneralSecurityContainer, adminTwitterSecurityContainer, siteUrl,
-    } = this.props;
-    const { isTwitterEnabled } = adminGeneralSecurityContainer.state;
-    const twitterCallbackUrl = urljoin(pathUtils.removeTrailingSlash(siteUrl), '/passport/twitter/callback');
-
-    return (
-
-      <React.Fragment>
-
-        <h2 className="alert-anchor border-bottom">
-          {t('security_settings.OAuth.Twitter.name')}
-        </h2>
-
-        {adminTwitterSecurityContainer.state.retrieveError != null && (
-          <div className="alert alert-danger">
-            <p>{t('Error occurred')} : {adminTwitterSecurityContainer.state.retrieveError}</p>
-          </div>
-        )}
-
-        <div className="form-group row">
-          <div className="col-6 offset-3">
-            <div className="custom-control custom-switch custom-checkbox-success">
-              <input
-                id="isTwitterEnabled"
-                className="custom-control-input"
-                type="checkbox"
-                checked={adminGeneralSecurityContainer.state.isTwitterEnabled}
-                onChange={() => { adminGeneralSecurityContainer.switchIsTwitterOAuthEnabled() }}
-              />
-              <label className="custom-control-label" htmlFor="isTwitterEnabled">
-                {t('security_settings.OAuth.Twitter.enable_twitter')}
-              </label>
-            </div>
-            {(!adminGeneralSecurityContainer.state.setupStrategies.includes('twitter') && isTwitterEnabled)
-              && <div className="badge badge-warning">{t('security_settings.setup_is_not_yet_complete')}</div>}
-          </div>
-        </div>
-
-        <div className="row mb-5">
-          <label className="col-md-3 text-md-right py-2">{t('security_settings.callback_URL')}</label>
-          <div className="col-md-6">
-            <input
-              className="form-control"
-              type="text"
-              value={twitterCallbackUrl}
-              readOnly
-            />
-            <p className="form-text text-muted small">{t('security_settings.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
-            {(siteUrl == null || siteUrl === '') && (
-              <div className="alert alert-danger">
-                <i
-                  className="icon-exclamation"
-                  // eslint-disable-next-line max-len
-                  dangerouslySetInnerHTML={{ __html: t('alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<i class="icon-login"></i></a>`, ns: 'commons' }) }}
-                />
-              </div>
-            )}
-          </div>
-        </div>
-
-
-        {isTwitterEnabled && (
-          <React.Fragment>
-
-            <h3 className="border-bottom">{t('security_settings.configuration')}</h3>
-
-            <div className="row mb-5">
-              <label htmlFor="TwitterConsumerId" className="col-md-3 text-md-right py-2">{t('security_settings.clientID')}</label>
-              <div className="col-md-6">
-                <input
-                  className="form-control"
-                  type="text"
-                  name="TwitterConsumerId"
-                  defaultValue={adminTwitterSecurityContainer.state.twitterConsumerKey || ''}
-                  onChange={e => adminTwitterSecurityContainer.changeTwitterConsumerKey(e.target.value)}
-                />
-                <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Use env var if empty', { env: 'OAUTH_TWITTER_CONSUMER_KEY' }) }} />
-                </p>
-              </div>
-            </div>
-
-            <div className="row mb-5">
-              <label htmlFor="TwitterConsumerSecret" className="col-md-3 text-md-right py-2">{t('security_settings.client_secret')}</label>
-              <div className="col-md-6">
-                <input
-                  className="form-control"
-                  type="text"
-                  name="TwitterConsumerSecret"
-                  defaultValue={adminTwitterSecurityContainer.state.twitterConsumerSecret || ''}
-                  onChange={e => adminTwitterSecurityContainer.changeTwitterConsumerSecret(e.target.value)}
-                />
-                <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Use env var if empty', { env: 'OAUTH_TWITTER_CONSUMER_SECRET' }) }} />
-                </p>
-              </div>
-            </div>
-
-            <div className="row mb-5">
-              <div className="offset-md-3 col-md-6">
-                <div className="custom-control custom-checkbox custom-checkbox-success">
-                  <input
-                    id="bindByUserNameTwitter"
-                    className="custom-control-input"
-                    type="checkbox"
-                    checked={adminTwitterSecurityContainer.state.isSameUsernameTreatedAsIdenticalUser || false}
-                    onChange={() => { adminTwitterSecurityContainer.switchIsSameUsernameTreatedAsIdenticalUser() }}
-                  />
-                  <label
-                    className="custom-control-label"
-                    htmlFor="bindByUserNameTwitter"
-                    dangerouslySetInnerHTML={{ __html: t('security_settings.Treat email matching as identical') }}
-                  />
-                </div>
-                <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Treat email matching as identical_warn') }} />
-                </p>
-              </div>
-            </div>
-
-            <div className="row my-3">
-              <div className="offset-4 col-5">
-                <button
-                  type="button"
-                  className="btn btn-primary"
-                  disabled={adminTwitterSecurityContainer.state.retrieveError != null}
-                  onClick={this.onClickSubmit}
-                >
-                  {t('Update')}
-                </button>
-              </div>
-            </div>
-
-          </React.Fragment>
-        )}
-
-        <hr />
-
-        <div style={{ minHeight: '300px' }}>
-          <h4>
-            <i className="icon-question" aria-hidden="true"></i>
-            <a href="#collapseHelpForTwitterOauth" data-toggle="collapse"> {t('security_settings.OAuth.how_to.twitter')}</a>
-          </h4>
-          <ol id="collapseHelpForTwitterOauth" className="collapse">
-            {/* eslint-disable-next-line max-len */}
-            <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.Twitter.register_1', { link: '<a href="https://apps.twitter.com/" target=_blank>Twitter Application Management</a>' }) }} />
-            <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.Twitter.register_2') }} />
-            <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.Twitter.register_3') }} />
-            {/* eslint-disable-next-line max-len */}
-            <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.Twitter.register_4', { url: adminTwitterSecurityContainer.state.callbackUrl }) }} />
-            <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.Twitter.register_5') }} />
-          </ol>
-        </div>
-
-      </React.Fragment>
-
-
-    );
-  }
-
-}
-
-TwitterSecuritySettingContents.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  adminGeneralSecurityContainer: PropTypes.instanceOf(AdminGeneralSecurityContainer).isRequired,
-  adminTwitterSecurityContainer: PropTypes.instanceOf(AdminTwitterSecurityContainer).isRequired,
-  siteUrl: PropTypes.string,
-};
-
-const TwitterSecuritySettingContentsWrapperFC = (props) => {
-  const { t } = useTranslation('admin');
-  const { data: siteUrl } = useSiteUrl();
-  return <TwitterSecuritySettingContents t={t} siteUrl={siteUrl} {...props} />;
-};
-
-/**
- * Wrapper component for using unstated
- */
-const TwitterSecuritySettingContentsWrapper = withUnstatedContainers(TwitterSecuritySettingContentsWrapperFC, [
-  AdminGeneralSecurityContainer,
-  AdminTwitterSecurityContainer,
-]);
-
-export default TwitterSecuritySettingContentsWrapper;

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

@@ -5,6 +5,7 @@ import {
   Dropdown, DropdownMenu, DropdownToggle, DropdownItem,
   Dropdown, DropdownMenu, DropdownToggle, DropdownItem,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
+import { NotAvailableForGuest } from '~/components/NotAvailableForGuest';
 import {
 import {
   IPageInfoAll, isIPageInfoForOperation,
   IPageInfoAll, isIPageInfoForOperation,
 } from '~/interfaces/page';
 } from '~/interfaces/page';
@@ -247,9 +248,10 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
   return (
   return (
     <DropdownMenu
     <DropdownMenu
       data-testid="page-item-control-menu"
       data-testid="page-item-control-menu"
-      positionFixed
       modifiers={{ preventOverflow: { boundariesElement: 'viewport' } }}
       modifiers={{ preventOverflow: { boundariesElement: 'viewport' } }}
       right={alignRight}
       right={alignRight}
+      container="body"
+      style={{ zIndex: 1055 }} /* make it larger than $zindex-modal of bootstrap */
     >
     >
       {contents}
       {contents}
     </DropdownMenu>
     </DropdownMenu>
@@ -330,24 +332,28 @@ export const PageItemControlSubstance = (props: PageItemControlSubstanceProps):
   }, [onClickPathRecoveryMenuItem, pageId]);
   }, [onClickPathRecoveryMenuItem, pageId]);
 
 
   return (
   return (
-    <Dropdown isOpen={isOpen} toggle={() => setIsOpen(!isOpen)} data-testid="open-page-item-control-btn">
-      { children ?? (
-        <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control d-flex align-items-center justify-content-center">
-          <i className="icon-options"></i>
-        </DropdownToggle>
-      ) }
-
-      <PageItemControlDropdownMenu
-        {...props}
-        isLoading={isLoading}
-        pageInfo={fetchedPageInfo ?? presetPageInfo}
-        onClickBookmarkMenuItem={bookmarkMenuItemClickHandler}
-        onClickRenameMenuItem={renameMenuItemClickHandler}
-        onClickDuplicateMenuItem={duplicateMenuItemClickHandler}
-        onClickDeleteMenuItem={deleteMenuItemClickHandler}
-        onClickPathRecoveryMenuItem={pathRecoveryMenuItemClickHandler}
-      />
-    </Dropdown>
+    <NotAvailableForGuest>
+      <Dropdown isOpen={isOpen} toggle={() => setIsOpen(!isOpen)} data-testid="open-page-item-control-btn">
+        { children ?? (
+          <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control d-flex align-items-center justify-content-center">
+            <i className="icon-options"></i>
+          </DropdownToggle>
+        ) }
+
+        <PageItemControlDropdownMenu
+          {...props}
+          isLoading={isLoading}
+          pageInfo={fetchedPageInfo ?? presetPageInfo}
+          onClickBookmarkMenuItem={bookmarkMenuItemClickHandler}
+          onClickRenameMenuItem={renameMenuItemClickHandler}
+          onClickDuplicateMenuItem={duplicateMenuItemClickHandler}
+          onClickDeleteMenuItem={deleteMenuItemClickHandler}
+          onClickPathRecoveryMenuItem={pathRecoveryMenuItemClickHandler}
+        />
+      </Dropdown>
+
+    </NotAvailableForGuest>
+
   );
   );
 
 
 };
 };

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

@@ -11,7 +11,7 @@ export const CompleteUserRegistration: FC = () => {
       <div className="row mx-0">
       <div className="row mx-0">
         <div className="col-12 mb-3 text-center">
         <div className="col-12 mb-3 text-center">
           <p className="alert alert-success">
           <p className="alert alert-success">
-            <span>{t('login.Registration successful')}</span>
+            <span>{t('login.registration_successful')}</span>
           </p>
           </p>
           {/* If the transition source is "/login", use <a /> tag since the transition will not occur if next/link is used. */}
           {/* If the transition source is "/login", use <a /> tag since the transition will not occur if next/link is used. */}
           <a href='/login'>
           <a href='/login'>

+ 1 - 1
packages/app/src/components/DescendantsPageListModal.module.scss

@@ -1,4 +1,4 @@
-.grw-page-accessories-modal :global {
+.grw-descendants-page-list-modal :global {
   .modal-header {
   .modal-header {
     button.close {
     button.close {
       margin: auto 0rem auto auto;
       margin: auto 0rem auto auto;

+ 2 - 2
packages/app/src/components/DescendantsPageListModal.tsx

@@ -93,8 +93,8 @@ export const DescendantsPageListModal = (): JSX.Element => {
       size="xl"
       size="xl"
       isOpen={isOpened}
       isOpen={isOpened}
       toggle={close}
       toggle={close}
-      data-testid="page-accessories-modal"
-      className={`grw-page-accessories-modal ${styles['grw-page-accessories-modal']} ${isWindowExpanded ? 'grw-modal-expanded' : ''} `}
+      data-testid="descendants-page-list-modal"
+      className={`grw-descendants-page-list-modal ${styles['grw-descendants-page-list-modal']} ${isWindowExpanded ? 'grw-modal-expanded' : ''} `}
     >
     >
       <ModalHeader className="p-0" toggle={close} close={buttons}>
       <ModalHeader className="p-0" toggle={close} close={buttons}>
         <CustomNavTab
         <CustomNavTab

+ 5 - 6
packages/app/src/components/Layout/AdminLayout.tsx

@@ -8,25 +8,24 @@ import { RawLayout } from './RawLayout';
 
 
 import styles from './Admin.module.scss';
 import styles from './Admin.module.scss';
 
 
+
+const AdminNavigation = dynamic(() => import('~/components/Admin/Common/AdminNavigation'), { ssr: false });
+const SystemVersion = dynamic(() => import('../SystemVersion'), { ssr: false });
 const HotkeysManager = dynamic(() => import('../Hotkeys/HotkeysManager'), { ssr: false });
 const HotkeysManager = dynamic(() => import('../Hotkeys/HotkeysManager'), { ssr: false });
 
 
 
 
 type Props = {
 type Props = {
-  title?: string
   componentTitle?: string
   componentTitle?: string
   children?: ReactNode
   children?: ReactNode
 }
 }
 
 
 
 
 const AdminLayout = ({
 const AdminLayout = ({
-  children, title, componentTitle,
+  children, componentTitle,
 }: Props): JSX.Element => {
 }: Props): JSX.Element => {
 
 
-  const AdminNavigation = dynamic(() => import('~/components/Admin/Common/AdminNavigation'), { ssr: false });
-  const SystemVersion = dynamic(() => import('../SystemVersion'), { ssr: false });
-
   return (
   return (
-    <RawLayout title={title}>
+    <RawLayout>
       <div className={`admin-page ${styles['admin-page']}`}>
       <div className={`admin-page ${styles['admin-page']}`}>
         <GrowiNavbar isGlobalSearchHidden={true} />
         <GrowiNavbar isGlobalSearchHidden={true} />
 
 

+ 15 - 12
packages/app/src/components/Layout/BasicLayout.tsx

@@ -4,6 +4,7 @@ import dynamic from 'next/dynamic';
 import { DndProvider } from 'react-dnd';
 import { DndProvider } from 'react-dnd';
 import { HTML5Backend } from 'react-dnd-html5-backend';
 import { HTML5Backend } from 'react-dnd-html5-backend';
 
 
+import { useEditorModeClassName } from '../../client/services/layout';
 import { GrowiNavbar } from '../Navbar/GrowiNavbar';
 import { GrowiNavbar } from '../Navbar/GrowiNavbar';
 import Sidebar from '../Sidebar';
 import Sidebar from '../Sidebar';
 
 
@@ -27,26 +28,18 @@ const Fab = dynamic(() => import('../Fab').then(mod => mod.Fab), { ssr: false })
 
 
 
 
 type Props = {
 type Props = {
-  title: string
-  className?: string,
-  expandContainer?: boolean,
   children?: ReactNode
   children?: ReactNode
+  className?: string
 }
 }
 
 
-export const BasicLayout = ({
-  children, title, className, expandContainer,
-}: Props): JSX.Element => {
-
-  const myClassName = `${className ?? ''} ${expandContainer ? 'growi-layout-fluid' : ''}`;
-
+export const BasicLayout = ({ children, className }: Props): JSX.Element => {
   return (
   return (
-    <RawLayout title={title} className={myClassName}>
-
+    <RawLayout className={className ?? ''}>
       <DndProvider backend={HTML5Backend}>
       <DndProvider backend={HTML5Backend}>
         <GrowiNavbar />
         <GrowiNavbar />
 
 
         <div className="page-wrapper d-flex d-print-block">
         <div className="page-wrapper d-flex d-print-block">
-          <div className="grw-sidebar-wrapper">
+          <div className="grw-sidebar-wrapper" data-testid="grw-sidebar-wrapper">
             <Sidebar />
             <Sidebar />
           </div>
           </div>
 
 
@@ -75,3 +68,13 @@ export const BasicLayout = ({
     </RawLayout>
     </RawLayout>
   );
   );
 };
 };
+
+export const BasicLayoutWithEditorMode = ({ children }: Props): JSX.Element => {
+  const className = useEditorModeClassName();
+
+  return (
+    <BasicLayout className={className}>
+      {children}
+    </BasicLayout>
+  );
+};

+ 0 - 8
packages/app/src/components/Layout/NoLoginLayout.module.scss

@@ -100,10 +100,6 @@
       rgba(#29487d, 0.4),
       rgba(#29487d, 0.4),
       $gray-700,
       $gray-700,
     ),
     ),
-    'twitter': (
-      rgba(#1da1f2, 0.4),
-      $gray-700,
-    ),
     'oidc': (
     'oidc': (
       rgba(#24292e, 0.4),
       rgba(#24292e, 0.4),
       $gray-700,
       $gray-700,
@@ -112,10 +108,6 @@
       rgba(#55a79a, 0.4),
       rgba(#55a79a, 0.4),
       $gray-700,
       $gray-700,
     ),
     ),
-    'basic': (
-      rgba(#24292e, 0.4),
-      $gray-700,
-    ),
   );
   );
 
 
   @each $label, $colors in $btn-fill-colors {
   @each $label, $colors in $btn-fill-colors {

+ 2 - 3
packages/app/src/components/Layout/NoLoginLayout.tsx

@@ -7,20 +7,19 @@ import { RawLayout } from './RawLayout';
 import commonStyles from './NoLoginLayout.module.scss';
 import commonStyles from './NoLoginLayout.module.scss';
 
 
 type Props = {
 type Props = {
-  title: string,
   className?: string,
   className?: string,
   children?: ReactNode,
   children?: ReactNode,
 }
 }
 
 
 export const NoLoginLayout = ({
 export const NoLoginLayout = ({
-  children, title, className,
+  children, className,
 }: Props): JSX.Element => {
 }: Props): JSX.Element => {
   const classNames: string[] = ['wrapper'];
   const classNames: string[] = ['wrapper'];
   if (className != null) {
   if (className != null) {
     classNames.push(className);
     classNames.push(className);
   }
   }
   return (
   return (
-    <RawLayout title={title} className={`${commonStyles.nologin}`}>
+    <RawLayout className={`${commonStyles.nologin}`}>
       <div className="nologin">
       <div className="nologin">
         <div id="wrapper">
         <div id="wrapper">
           <div id="page-wrapper">
           <div id="page-wrapper">

+ 1 - 3
packages/app/src/components/Layout/RawLayout.tsx

@@ -12,12 +12,11 @@ const logger = loggerFactory('growi:cli:RawLayout');
 
 
 
 
 type Props = {
 type Props = {
-  title?: string,
   className?: string,
   className?: string,
   children?: ReactNode,
   children?: ReactNode,
 }
 }
 
 
-export const RawLayout = ({ children, title, className }: Props): JSX.Element => {
+export const RawLayout = ({ children, className }: Props): JSX.Element => {
   const classNames: string[] = ['layout-root', 'growi'];
   const classNames: string[] = ['layout-root', 'growi'];
   if (className != null) {
   if (className != null) {
     classNames.push(className);
     classNames.push(className);
@@ -35,7 +34,6 @@ export const RawLayout = ({ children, title, className }: Props): JSX.Element =>
   return (
   return (
     <>
     <>
       <Head>
       <Head>
-        <title>{title}</title>
         <meta charSet="utf-8" />
         <meta charSet="utf-8" />
         <meta name="viewport" content="initial-scale=1.0, width=device-width" />
         <meta name="viewport" content="initial-scale=1.0, width=device-width" />
       </Head>
       </Head>

+ 2 - 11
packages/app/src/components/Layout/SearchResultLayout.tsx

@@ -5,23 +5,14 @@ import { BasicLayout } from '~/components/Layout/BasicLayout';
 import commonStyles from './SearchResultLayout.module.scss';
 import commonStyles from './SearchResultLayout.module.scss';
 
 
 type Props = {
 type Props = {
-  title: string,
-  className?: string,
   children?: ReactNode,
   children?: ReactNode,
 }
 }
 
 
-const SearchResultLayout = ({
-  children, title, className,
-}: Props): JSX.Element => {
-
-  const classNames: string[] = [];
-  if (className != null) {
-    classNames.push(className);
-  }
+const SearchResultLayout = ({ children }: Props): JSX.Element => {
 
 
   return (
   return (
     <div className={`on-search ${commonStyles['on-search']}`}>
     <div className={`on-search ${commonStyles['on-search']}`}>
-      <BasicLayout title={title} className={classNames.join(' ')}>
+      <BasicLayout>
         <div id="grw-fav-sticky-trigger" className="sticky-top"></div>
         <div id="grw-fav-sticky-trigger" className="sticky-top"></div>
         <div id="main" className="main search-page mt-0">
         <div id="main" className="main search-page mt-0">
           { children }
           { children }

+ 4 - 9
packages/app/src/components/Layout/ShareLinkLayout.tsx

@@ -2,6 +2,7 @@ import React, { ReactNode } from 'react';
 
 
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 
 
+import { useEditorModeClassName } from '../../client/services/layout';
 import { GrowiNavbar } from '../Navbar/GrowiNavbar';
 import { GrowiNavbar } from '../Navbar/GrowiNavbar';
 
 
 import { RawLayout } from './RawLayout';
 import { RawLayout } from './RawLayout';
@@ -16,20 +17,14 @@ const Fab = dynamic(() => import('../Fab').then(mod => mod.Fab), { ssr: false })
 
 
 
 
 type Props = {
 type Props = {
-  title: string
-  className?: string,
-  expandContainer?: boolean,
   children?: ReactNode
   children?: ReactNode
 }
 }
 
 
-export const ShareLinkLayout = ({
-  children, title, className, expandContainer,
-}: Props): JSX.Element => {
-
-  const myClassName = `${className ?? ''} ${expandContainer ? 'growi-layout-fluid' : ''}`;
+export const ShareLinkLayout = ({ children }: Props): JSX.Element => {
+  const className = useEditorModeClassName();
 
 
   return (
   return (
-    <RawLayout title={title} className={myClassName}>
+    <RawLayout className={className}>
       <GrowiNavbar isGlobalSearchHidden={true} />
       <GrowiNavbar isGlobalSearchHidden={true} />
 
 
       <div className="page-wrapper d-flex d-print-block">
       <div className="page-wrapper d-flex d-print-block">

+ 13 - 5
packages/app/src/components/LoginForm.tsx

@@ -2,6 +2,7 @@ import React, {
   useState, useEffect, useCallback,
   useState, useEffect, useCallback,
 } from 'react';
 } from 'react';
 
 
+import { USER_STATUS } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
 import ReactCardFlip from 'react-card-flip';
 import ReactCardFlip from 'react-card-flip';
@@ -90,8 +91,17 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
 
 
     try {
     try {
       const res = await apiv3Post('/login', { loginForm });
       const res = await apiv3Post('/login', { loginForm });
-      const { redirectTo } = res.data;
-      router.push(redirectTo ?? '/');
+      const { redirectTo, userStatus } = res.data;
+
+      if (redirectTo != null) {
+        return router.push(redirectTo);
+      }
+
+      if (userStatus !== USER_STATUS.ACTIVE) {
+        window.location.href = '/';
+      }
+
+      return router.push('/');
     }
     }
     catch (err) {
     catch (err) {
       const errs = toArrayIfNot(err);
       const errs = toArrayIfNot(err);
@@ -217,10 +227,8 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
       google: 'google',
       google: 'google',
       github: 'github',
       github: 'github',
       facebook: 'facebook',
       facebook: 'facebook',
-      twitter: 'twitter',
       oidc: 'openid',
       oidc: 'openid',
       saml: 'key',
       saml: 'key',
-      basic: 'lock',
     };
     };
 
 
     return (
     return (
@@ -497,7 +505,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
   }
   }
 
 
   return (
   return (
-    <div className="noLogin-dialog mx-auto" id="noLogin-dialog">
+    <div className="noLogin-dialog mx-auto" id="noLogin-dialog" data-testid="login-form">
       <div className="row mx-0">
       <div className="row mx-0">
         <div className="col-12">
         <div className="col-12">
           <ReactCardFlip isFlipped={isRegistering} flipDirection="horizontal" cardZIndex="3">
           <ReactCardFlip isFlipped={isRegistering} flipDirection="horizontal" cardZIndex="3">

+ 2 - 8
packages/app/src/components/Me/AssociateModal.tsx

@@ -80,18 +80,12 @@ const AssociateModal = (props: Props): JSX.Element => {
             >
             >
               <i className="fa fa-google"></i> (TBD) Google OAuth
               <i className="fa fa-google"></i> (TBD) Google OAuth
             </NavLink>
             </NavLink>
-            <NavLink
+            {/* <NavLink
               className={activeTab === 4 ? 'active' : ''}
               className={activeTab === 4 ? 'active' : ''}
               onClick={() => setActiveTab(4)}
               onClick={() => setActiveTab(4)}
             >
             >
               <i className="fa fa-facebook"></i> (TBD) Facebook
               <i className="fa fa-facebook"></i> (TBD) Facebook
-            </NavLink>
-            <NavLink
-              className={activeTab === 5 ? 'active' : ''}
-              onClick={() => setActiveTab(5)}
-            >
-              <i className="fa fa-twitter"></i> (TBD) Twitter
-            </NavLink>
+            </NavLink> */}
           </Nav>
           </Nav>
           <TabContent activeTab={activeTab}>
           <TabContent activeTab={activeTab}>
             <TabPane tabId={1}>
             <TabPane tabId={1}>

+ 8 - 2
packages/app/src/components/Navbar/AppearanceModeDropdown.tsx

@@ -113,10 +113,16 @@ export const AppearanceModeDropdown:FC<AppearanceModeDropdownProps> = (props: Ap
       <div className="dropdown-menu dropdown-menu-right">
       <div className="dropdown-menu dropdown-menu-right">
 
 
         {/* sidebar mode */}
         {/* sidebar mode */}
-        {[renderSidebarModeSwitch(false), dropdownDivider]}
+        {renderSidebarModeSwitch(false)}
+        {dropdownDivider}
 
 
         {/* side bar mode on editor */}
         {/* side bar mode on editor */}
-        {isAuthenticated && [renderSidebarModeSwitch(true), dropdownDivider]}
+        {isAuthenticated && (
+          <>
+            {renderSidebarModeSwitch(true)}
+            {dropdownDivider}
+          </>
+        )}
 
 
         {/* color mode */}
         {/* color mode */}
         <h6 className="dropdown-header">{t('personal_dropdown.color_mode')}</h6>
         <h6 className="dropdown-header">{t('personal_dropdown.color_mode')}</h6>

+ 13 - 15
packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -6,13 +6,12 @@ import dynamic from 'next/dynamic';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
 import { DropdownItem } from 'reactstrap';
 import { DropdownItem } from 'reactstrap';
 
 
-import { exportAsMarkdown, updateContentWidth } from '~/client/services/page-operation';
+import { exportAsMarkdown, updateContentWidth, useUpdateStateAfterSave } from '~/client/services/page-operation';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiPost } from '~/client/util/apiv1-client';
 import { apiPost } from '~/client/util/apiv1-client';
 import {
 import {
   IPageToRenameWithMeta, IPageWithMeta, IPageInfoForEntity,
   IPageToRenameWithMeta, IPageWithMeta, IPageInfoForEntity,
 } from '~/interfaces/page';
 } from '~/interfaces/page';
-import { IResTagsUpdateApiv1 } from '~/interfaces/tag';
 import { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
 import { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
 import {
 import {
   useCurrentPageId, useCurrentPathname, useIsNotFound,
   useCurrentPageId, useCurrentPathname, useIsNotFound,
@@ -191,13 +190,15 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
   const { data: shareLinkId } = useShareLinkId();
   const { data: shareLinkId } = useShareLinkId();
   const { data: currentPage, mutate: mutateCurrentPage } = useSWRxCurrentPage(shareLinkId ?? undefined);
   const { data: currentPage, mutate: mutateCurrentPage } = useSWRxCurrentPage(shareLinkId ?? undefined);
 
 
+  const { data: currentPathname } = useCurrentPathname();
+  const isSharedPage = pagePathUtils.isSharedPage(currentPathname ?? '');
+
   const revision = currentPage?.revision;
   const revision = currentPage?.revision;
   const revisionId = (revision != null && isPopulated(revision)) ? revision._id : undefined;
   const revisionId = (revision != null && isPopulated(revision)) ? revision._id : undefined;
 
 
   const { data: isDrawerMode } = useDrawerMode();
   const { data: isDrawerMode } = useDrawerMode();
   const { data: editorMode, mutate: mutateEditorMode } = useEditorMode();
   const { data: editorMode, mutate: mutateEditorMode } = useEditorMode();
   const { data: pageId } = useCurrentPageId();
   const { data: pageId } = useCurrentPageId();
-  const { data: currentPathname } = useCurrentPathname();
   const { data: currentUser } = useCurrentUser();
   const { data: currentUser } = useCurrentUser();
   const { data: isNotFound } = useIsNotFound();
   const { data: isNotFound } = useIsNotFound();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
@@ -209,14 +210,18 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
   const { data: isAbleToShowPageEditorModeManager } = useIsAbleToShowPageEditorModeManager();
   const { data: isAbleToShowPageEditorModeManager } = useIsAbleToShowPageEditorModeManager();
   const { data: isAbleToShowPageAuthors } = useIsAbleToShowPageAuthors();
   const { data: isAbleToShowPageAuthors } = useIsAbleToShowPageAuthors();
 
 
-  const { mutate: mutateSWRTagsInfo, data: tagsInfoData } = useSWRxTagsInfo(currentPage?._id);
-  const { data: tagsForEditors, mutate: mutatePageTagsForEditors, sync: syncPageTagsForEditors } = usePageTagsForEditors(currentPage?._id);
+  const { mutate: mutateSWRTagsInfo, data: tagsInfoData } = useSWRxTagsInfo(!isSharedPage ? currentPage?._id : undefined);
+
+  // eslint-disable-next-line max-len
+  const { data: tagsForEditors, mutate: mutatePageTagsForEditors, sync: syncPageTagsForEditors } = usePageTagsForEditors(!isSharedPage ? currentPage?._id : undefined);
 
 
   const { open: openDuplicateModal } = usePageDuplicateModal();
   const { open: openDuplicateModal } = usePageDuplicateModal();
   const { open: openRenameModal } = usePageRenameModal();
   const { open: openRenameModal } = usePageRenameModal();
   const { open: openDeleteModal } = usePageDeleteModal();
   const { open: openDeleteModal } = usePageDeleteModal();
   const { data: templateTagData } = useTemplateTagData();
   const { data: templateTagData } = useTemplateTagData();
 
 
+  const updateStateAfterSave = useUpdateStateAfterSave(pageId);
+
   const path = currentPage?.path ?? currentPathname;
   const path = currentPage?.path ?? currentPathname;
 
 
   useEffect(() => {
   useEffect(() => {
@@ -247,16 +252,9 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
 
 
     const { _id: pageId, revision: revisionId } = currentPage;
     const { _id: pageId, revision: revisionId } = currentPage;
     try {
     try {
-      const res: IResTagsUpdateApiv1 = await apiPost('/tags.update', { pageId, revisionId, tags: newTags });
-      mutateCurrentPage();
-
-      // TODO: fix https://github.com/weseek/growi/pull/6478 without pageContainer
-      // const lastUpdateUser = res.savedPage?.lastUpdateUser as IUser;
-      // await pageContainer.setState({ lastUpdateUsername: lastUpdateUser.username });
+      await apiPost('/tags.update', { pageId, revisionId, tags: newTags });
 
 
-      // revalidate SWRTagsInfo
-      mutateSWRTagsInfo();
-      mutatePageTagsForEditors(newTags);
+      updateStateAfterSave?.();
 
 
       toastSuccess('updated tags successfully');
       toastSuccess('updated tags successfully');
     }
     }
@@ -264,7 +262,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
       toastError(err, 'fail to update tags');
       toastError(err, 'fail to update tags');
     }
     }
 
 
-  }, [currentPage, mutateCurrentPage, mutateSWRTagsInfo, mutatePageTagsForEditors]);
+  }, [currentPage, updateStateAfterSave]);
 
 
   const tagsUpdatedHandlerForEditMode = useCallback((newTags: string[]): void => {
   const tagsUpdatedHandlerForEditMode = useCallback((newTags: string[]): void => {
     // It will not be reflected in the DB until the page is refreshed
     // It will not be reflected in the DB until the page is refreshed

+ 1 - 1
packages/app/src/components/Navbar/GrowiNavbar.tsx

@@ -80,7 +80,7 @@ const NavbarRight = memo((): JSX.Element => {
         <li className="grw-apperance-mode-dropdown nav-item dropdown">
         <li className="grw-apperance-mode-dropdown nav-item dropdown">
           <AppearanceModeDropdown isAuthenticated={isAuthenticated} />
           <AppearanceModeDropdown isAuthenticated={isAuthenticated} />
         </li>
         </li>
-        <li id="login-user" className="nav-item"><a className="nav-link" href="/login">Login</a></li>;
+        <li id="login-user" className="nav-item"><a className="nav-link" href="/login">Login</a></li>
       </>
       </>
     );
     );
   }, [isAuthenticated]);
   }, [isAuthenticated]);

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

@@ -34,7 +34,7 @@ const PersonalDropdown = () => {
       {/* Button */}
       {/* Button */}
       {/* remove .dropdown-toggle for hide caret */}
       {/* remove .dropdown-toggle for hide caret */}
       {/* See https://stackoverflow.com/a/44577512/13183572 */}
       {/* See https://stackoverflow.com/a/44577512/13183572 */}
-      <button className="bg-transparent border-0 nav-link" type="button" ref={buttonRef} data-toggle="dropdown">
+      <button className="bg-transparent border-0 nav-link" type="button" ref={buttonRef} data-toggle="dropdown" data-testid="personal-dropdown-button">
         <UserPicture user={user} noLink noTooltip /><span className="ml-1 d-none d-lg-inline-block">&nbsp;{user.name}</span>
         <UserPicture user={user} noLink noTooltip /><span className="ml-1 d-none d-lg-inline-block">&nbsp;{user.name}</span>
       </button>
       </button>
 
 

+ 9 - 3
packages/app/src/components/PageAlert/PageRedirectedAlert.tsx

@@ -2,24 +2,30 @@ import React, { useState, useCallback } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
+import { unlink } from '~/client/services/page-operation';
 import { toastError } from '~/client/util/apiNotification';
 import { toastError } from '~/client/util/apiNotification';
+import { useCurrentPagePath } from '~/stores/page';
 import { useRedirectFrom } from '~/stores/page-redirect';
 import { useRedirectFrom } from '~/stores/page-redirect';
 
 
 export const PageRedirectedAlert = React.memo((): JSX.Element => {
 export const PageRedirectedAlert = React.memo((): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
-  const { data: redirectFrom, unlink } = useRedirectFrom();
+  const { data: currentPagePath } = useCurrentPagePath();
+  const { data: redirectFrom } = useRedirectFrom();
 
 
   const [isUnlinked, setIsUnlinked] = useState(false);
   const [isUnlinked, setIsUnlinked] = useState(false);
 
 
   const unlinkButtonClickHandler = useCallback(async() => {
   const unlinkButtonClickHandler = useCallback(async() => {
+    if (currentPagePath == null) {
+      return;
+    }
     try {
     try {
-      await unlink();
+      await unlink(currentPagePath);
       setIsUnlinked(true);
       setIsUnlinked(true);
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
     }
     }
-  }, [unlink]);
+  }, [currentPagePath]);
 
 
   if (redirectFrom == null || redirectFrom === '') {
   if (redirectFrom == null || redirectFrom === '') {
     return <></>;
     return <></>;

+ 10 - 5
packages/app/src/components/PageAlert/TrashPageAlert.tsx

@@ -5,10 +5,12 @@ import { format } from 'date-fns';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
+import { unlink } from '~/client/services/page-operation';
 import { toastError } from '~/client/util/apiNotification';
 import { toastError } from '~/client/util/apiNotification';
 import { usePageDeleteModal, usePutBackPageModal } from '~/stores/modal';
 import { usePageDeleteModal, usePutBackPageModal } from '~/stores/modal';
-import { useSWRxPageInfo, useSWRxCurrentPage, useIsTrashPage } from '~/stores/page';
-import { useRedirectFrom } from '~/stores/page-redirect';
+import {
+  useCurrentPagePath, useSWRxPageInfo, useSWRxCurrentPage, useIsTrashPage,
+} from '~/stores/page';
 import { useIsAbleToShowTrashPageManagementButtons } from '~/stores/ui';
 import { useIsAbleToShowTrashPageManagementButtons } from '~/stores/ui';
 
 
 
 
@@ -34,7 +36,7 @@ export const TrashPageAlert = (): JSX.Element => {
 
 
   const { open: openDeleteModal } = usePageDeleteModal();
   const { open: openDeleteModal } = usePageDeleteModal();
   const { open: openPutBackPageModal } = usePutBackPageModal();
   const { open: openPutBackPageModal } = usePutBackPageModal();
-  const { unlink } = useRedirectFrom();
+  const { data: currentPagePath } = useCurrentPagePath();
 
 
 
 
   const deleteUser = pageData?.deleteUser;
   const deleteUser = pageData?.deleteUser;
@@ -47,8 +49,11 @@ export const TrashPageAlert = (): JSX.Element => {
       return;
       return;
     }
     }
     const putBackedHandler = () => {
     const putBackedHandler = () => {
+      if (currentPagePath == null) {
+        return;
+      }
       try {
       try {
-        unlink();
+        unlink(currentPagePath);
         // Do not use "router.push(`/${pageId}`)" to avoid `Error: Invariant: attempted to hard navigate to the same URL`
         // Do not use "router.push(`/${pageId}`)" to avoid `Error: Invariant: attempted to hard navigate to the same URL`
         // See: https://github.com/weseek/growi/pull/7054
         // See: https://github.com/weseek/growi/pull/7054
         router.reload();
         router.reload();
@@ -58,7 +63,7 @@ export const TrashPageAlert = (): JSX.Element => {
       }
       }
     };
     };
     openPutBackPageModal({ pageId, path: pagePath }, { onPutBacked: putBackedHandler });
     openPutBackPageModal({ pageId, path: pagePath }, { onPutBacked: putBackedHandler });
-  }, [openPutBackPageModal, pageId, pagePath, router, unlink]);
+  }, [currentPagePath, openPutBackPageModal, pageId, pagePath, router]);
 
 
   const openPageDeleteModalHandler = useCallback(() => {
   const openPageDeleteModalHandler = useCallback(() => {
     if (pageId === undefined || revisionId === undefined || pagePath === undefined) {
     if (pageId === undefined || revisionId === undefined || pagePath === undefined) {

+ 15 - 12
packages/app/src/components/PageComment.tsx

@@ -13,6 +13,7 @@ import { useCommentForCurrentPageOptions } from '~/stores/renderer';
 import { ICommentHasId, ICommentHasIdList } from '../interfaces/comment';
 import { ICommentHasId, ICommentHasIdList } from '../interfaces/comment';
 import { useSWRxPageComment } from '../stores/comment';
 import { useSWRxPageComment } from '../stores/comment';
 
 
+import { NotAvailableForGuest } from './NotAvailableForGuest';
 import { Comment } from './PageComment/Comment';
 import { Comment } from './PageComment/Comment';
 import { CommentEditor } from './PageComment/CommentEditor';
 import { CommentEditor } from './PageComment/CommentEditor';
 import { DeleteCommentModal } from './PageComment/DeleteCommentModal';
 import { DeleteCommentModal } from './PageComment/DeleteCommentModal';
@@ -173,18 +174,20 @@ export const PageComment: FC<PageCommentProps> = memo((props:PageCommentProps):
                   {commentElement(comment)}
                   {commentElement(comment)}
                   {hasReply && replyCommentsElement(allReplies[comment._id])}
                   {hasReply && replyCommentsElement(allReplies[comment._id])}
                   {(!isReadOnly && !showEditorIds.has(comment._id)) && (
                   {(!isReadOnly && !showEditorIds.has(comment._id)) && (
-                    <div className="text-right">
-                      <Button
-                        outline
-                        color="secondary"
-                        size="sm"
-                        className="btn-comment-reply"
-                        onClick={() => {
-                          setShowEditorIds(previousState => new Set(previousState.add(comment._id)));
-                        }}
-                      >
-                        <i className="icon-fw icon-action-undo"></i> Reply
-                      </Button>
+                    <div className="d-flex flex-row-reverse">
+                      <NotAvailableForGuest>
+                        <Button
+                          outline
+                          color="secondary"
+                          size="sm"
+                          className="btn-comment-reply"
+                          onClick={() => {
+                            setShowEditorIds(previousState => new Set(previousState.add(comment._id)));
+                          }}
+                        >
+                          <i className="icon-fw icon-action-undo"></i> Reply
+                        </Button>
+                      </NotAvailableForGuest>
                     </div>
                     </div>
                   )}
                   )}
                   {(!isReadOnly && showEditorIds.has(comment._id)) && (
                   {(!isReadOnly && showEditorIds.has(comment._id)) && (

+ 1 - 1
packages/app/src/components/PageEditor/Cheatsheet.tsx

@@ -17,7 +17,7 @@ export const Cheatsheet = (): JSX.Element => {
   const codeBlockStr = 'text\n\ntext';
   const codeBlockStr = 'text\n\ntext';
   const lineBlockStr = 'text\ntext';
   const lineBlockStr = 'text\ntext';
   const typographyStr = `*${t('sandbox.italics')}*\n**${t('sandbox.bold')}**\n***${t('sandbox.italic_bold')}***\n~~${t('sandbox.strikethrough')}~~`;
   const typographyStr = `*${t('sandbox.italics')}*\n**${t('sandbox.bold')}**\n***${t('sandbox.italic_bold')}***\n~~${t('sandbox.strikethrough')}~~`;
-  const linkStr = '[Google](https://www.google.co.jp/)\n[/Page1/ChildPage1]';
+  const linkStr = '[Google](https://www.google.co.jp/)';
   const codeHighlightStr = '```javascript:index.js\nwriteCode();\n```';
   const codeHighlightStr = '```javascript:index.js\nwriteCode();\n```';
 
 
   // Right Side
   // Right Side

+ 3 - 3
packages/app/src/components/PageEditor/Editor.tsx

@@ -227,7 +227,7 @@ const Editor: ForwardRefRenderFunction<IEditorMethods, EditorPropsType> = (props
     );
     );
   }, [isUploading]);
   }, [isUploading]);
 
 
-  const renderNavbar = useCallback(() => {
+  const renderNavbar = () => {
     return (
     return (
       <div className="m-0 navbar navbar-default navbar-editor" data-testid="navbar-editor" style={{ minHeight: 'unset' }}>
       <div className="m-0 navbar navbar-default navbar-editor" data-testid="navbar-editor" style={{ minHeight: 'unset' }}>
         <ul className="pl-2 nav nav-navbar">
         <ul className="pl-2 nav nav-navbar">
@@ -238,7 +238,7 @@ const Editor: ForwardRefRenderFunction<IEditorMethods, EditorPropsType> = (props
         </ul>
         </ul>
       </div>
       </div>
     );
     );
-  }, [editorSubstance]);
+  };
 
 
   const renderCheatsheetModal = useCallback(() => {
   const renderCheatsheetModal = useCallback(() => {
     const hideCheatsheetModal = () => {
     const hideCheatsheetModal = () => {
@@ -246,7 +246,7 @@ const Editor: ForwardRefRenderFunction<IEditorMethods, EditorPropsType> = (props
     };
     };
 
 
     return (
     return (
-      <Modal isOpen={isCheatsheetModalShown} toggle={hideCheatsheetModal} className={`modal-gfm-cheatsheet ${styles['modal-gfm-cheatsheet']}`} >
+      <Modal isOpen={isCheatsheetModalShown} toggle={hideCheatsheetModal} className={`modal-gfm-cheatsheet ${styles['modal-gfm-cheatsheet']}`} size={'lg'} >
         <ModalHeader tag="h4" toggle={hideCheatsheetModal} className="bg-primary text-light">
         <ModalHeader tag="h4" toggle={hideCheatsheetModal} className="bg-primary text-light">
           <i className="icon-fw icon-question" />Markdown help
           <i className="icon-fw icon-question" />Markdown help
         </ModalHeader>
         </ModalHeader>

+ 20 - 33
packages/app/src/components/PageEditor/LinkEditModal.jsx

@@ -345,7 +345,7 @@ class LinkEditModal extends React.PureComponent {
     return (
     return (
       <div className="card well pt-3">
       <div className="card well pt-3">
         <form className="form-group mb-0">
         <form className="form-group mb-0">
-          <div className="form-group row">
+          <div className="form-group mb-0 row">
             <label className="col-sm-3">{t('link_edit.path_format')}</label>
             <label className="col-sm-3">{t('link_edit.path_format')}</label>
             <div className="col-sm-9">
             <div className="col-sm-9">
               <div className="custom-control custom-checkbox custom-checkbox-info custom-control-inline">
               <div className="custom-control custom-checkbox custom-checkbox-info custom-control-inline">
@@ -376,36 +376,23 @@ class LinkEditModal extends React.PureComponent {
               </div>
               </div>
             </div>
             </div>
           </div>
           </div>
-          <div className="form-group row mb-0">
-            <label className="col-sm-3">{t('link_edit.notation')}</label>
-            <div className="col-sm-9">
-              <div className="custom-control custom-radio custom-control-inline">
-                <input
-                  type="radio"
-                  className="custom-control-input"
-                  id="markdownType"
-                  value={Linker.types.markdownLink}
-                  checked={this.state.linkerType === Linker.types.markdownLink}
-                  onChange={e => this.handleSelecteLinkerType(e.target.value)}
-                />
-                <label className="custom-control-label" htmlFor="markdownType">
-                  {t('link_edit.markdown')}
-                </label>
-              </div>
-              <div className="custom-control custom-radio custom-control-inline">
-                <input
-                  type="radio"
-                  className="custom-control-input"
-                  id="growiType"
-                  value={Linker.types.growiLink}
-                  checked={this.state.linkerType === Linker.types.growiLink}
-                  onChange={e => this.handleSelecteLinkerType(e.target.value)}
-                />
-                <label className="custom-control-label" htmlFor="growiType">
-                  {t('link_edit.GROWI_original')}
-                </label>
-              </div>
-              {this.isApplyPukiwikiLikeLinkerPlugin && (
+          {this.isApplyPukiwikiLikeLinkerPlugin && (
+            <div className="form-group row mb-0 mt-1">
+              <label className="col-sm-3">{t('link_edit.notation')}</label>
+              <div className="col-sm-9">
+                <div className="custom-control custom-radio custom-control-inline">
+                  <input
+                    type="radio"
+                    className="custom-control-input"
+                    id="markdownType"
+                    value={Linker.types.markdownLink}
+                    checked={this.state.linkerType === Linker.types.markdownLink}
+                    onChange={e => this.handleSelecteLinkerType(e.target.value)}
+                  />
+                  <label className="custom-control-label" htmlFor="markdownType">
+                    {t('link_edit.markdown')}
+                  </label>
+                </div>
                 <div className="custom-control custom-radio custom-control-inline">
                 <div className="custom-control custom-radio custom-control-inline">
                   <input
                   <input
                     type="radio"
                     type="radio"
@@ -419,9 +406,9 @@ class LinkEditModal extends React.PureComponent {
                     {t('link_edit.pukiwiki')}
                     {t('link_edit.pukiwiki')}
                   </label>
                   </label>
                 </div>
                 </div>
-              )}
+              </div>
             </div>
             </div>
-          </div>
+          )}
         </form>
         </form>
       </div>
       </div>
     );
     );

+ 0 - 25
packages/app/src/components/PageEditor/OptionsSelector.tsx

@@ -199,30 +199,6 @@ const ConfigurationDropdown = memo(({ onConfirmEnableTextlint }: ConfigurationDr
     );
     );
   }, [editorSettings, update, t]);
   }, [editorSettings, update, t]);
 
 
-  const renderRealtimeDrawioMenuItem = useCallback(() => {
-    if (editorSettings == null) {
-      return <></>;
-    }
-
-    const isActive = editorSettings.renderDrawioInRealtime;
-
-    const iconClasses = ['text-info'];
-    if (isActive) {
-      iconClasses.push('ti ti-check');
-    }
-    const iconClassName = iconClasses.join(' ');
-
-    return (
-      <DropdownItem toggle={false} onClick={() => update({ renderDrawioInRealtime: !isActive })}>
-        <div className="d-flex justify-content-between">
-          <span className="icon-container"><img src="/images/icons/fx.svg" width="14px" alt="fx"></img></span>
-          <span className="menuitem-label">draw.io Rendering</span>
-          <span className="icon-container"><i className={iconClassName}></i></span>
-        </div>
-      </DropdownItem>
-    );
-  }, [editorSettings, update]);
-
   const renderMarkdownTableAutoFormattingMenuItem = useCallback(() => {
   const renderMarkdownTableAutoFormattingMenuItem = useCallback(() => {
     if (editorSettings == null) {
     if (editorSettings == null) {
       return <></>;
       return <></>;
@@ -300,7 +276,6 @@ const ConfigurationDropdown = memo(({ onConfirmEnableTextlint }: ConfigurationDr
 
 
         <DropdownMenu>
         <DropdownMenu>
           {renderActiveLineMenuItem()}
           {renderActiveLineMenuItem()}
-          {renderRealtimeDrawioMenuItem()}
           {renderMarkdownTableAutoFormattingMenuItem()}
           {renderMarkdownTableAutoFormattingMenuItem()}
           {renderIsTextlintEnabledMenuItem()}
           {renderIsTextlintEnabledMenuItem()}
           {/* <DropdownItem divider /> */}
           {/* <DropdownItem divider /> */}

+ 9 - 5
packages/app/src/components/PageEditorByHackmd.tsx

@@ -117,8 +117,6 @@ export const PageEditorByHackmd = (): JSX.Element => {
       const markdown = await hackmdEditorRef.current.getValue();
       const markdown = await hackmdEditorRef.current.getValue();
 
 
       const { page } = await saveOrUpdate(markdown, { pageId, path: currentPagePath || currentPathname, revisionId: revisionIdHackmdSynced }, options);
       const { page } = await saveOrUpdate(markdown, { pageId, path: currentPagePath || currentPathname, revisionId: revisionIdHackmdSynced }, options);
-      await mutatePageData();
-      await mutateTagsInfo();
 
 
       if (page == null) {
       if (page == null) {
         return;
         return;
@@ -128,6 +126,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
       }
       }
       else {
       else {
         updateStateAfterSave?.();
         updateStateAfterSave?.();
+        mutateIsHackmdDraftUpdatingInRealtime(false);
       }
       }
       setIsInitialized(false);
       setIsInitialized(false);
       mutateEditorMode(EditorMode.View);
       mutateEditorMode(EditorMode.View);
@@ -137,7 +136,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
       toastError(error.message);
       toastError(error.message);
     }
     }
   // eslint-disable-next-line max-len
   // eslint-disable-next-line max-len
-  }, [editorMode, currentPathname, revision, revisionIdHackmdSynced, optionsToSave, saveOrUpdate, pageId, currentPagePath, mutatePageData, mutateTagsInfo, isNotFound, mutateEditorMode, router, updateStateAfterSave]);
+  }, [editorMode, currentPathname, revision, revisionIdHackmdSynced, optionsToSave, saveOrUpdate, pageId, currentPagePath, isNotFound, mutateEditorMode, router, updateStateAfterSave, mutateIsHackmdDraftUpdatingInRealtime]);
 
 
   // set handler to save and reload Page
   // set handler to save and reload Page
   useEffect(() => {
   useEffect(() => {
@@ -259,6 +258,8 @@ export const PageEditorByHackmd = (): JSX.Element => {
       updateStateAfterSave?.();
       updateStateAfterSave?.();
       mutateTagsInfo();
       mutateTagsInfo();
 
 
+      mutateIsEnabledUnsavedWarning(false);
+
       logger.debug('success to save');
       logger.debug('success to save');
 
 
       toastSuccess(t('successfully_saved_the_page'));
       toastSuccess(t('successfully_saved_the_page'));
@@ -267,7 +268,8 @@ export const PageEditorByHackmd = (): JSX.Element => {
       logger.error('failed to save', error);
       logger.error('failed to save', error);
       toastError(error.message);
       toastError(error.message);
     }
     }
-  }, [currentPagePath, currentPathname, pageId, revisionIdHackmdSynced, optionsToSave, saveOrUpdate, mutatePageData, updateStateAfterSave, mutateTagsInfo, t]);
+  }, [currentPagePath, currentPathname, pageId, revisionIdHackmdSynced, optionsToSave,
+      saveOrUpdate, mutatePageData, updateStateAfterSave, mutateTagsInfo, mutateIsEnabledUnsavedWarning, t]);
 
 
   /**
   /**
    * onChange event of HackmdEditor handler
    * onChange event of HackmdEditor handler
@@ -283,13 +285,15 @@ export const PageEditorByHackmd = (): JSX.Element => {
       return;
       return;
     }
     }
 
 
+    mutateIsEnabledUnsavedWarning(true);
+
     try {
     try {
       await apiPost('/hackmd.saveOnHackmd', { pageId });
       await apiPost('/hackmd.saveOnHackmd', { pageId });
     }
     }
     catch (err) {
     catch (err) {
       logger.error(err);
       logger.error(err);
     }
     }
-  }, [pageId, revision?.body, hackmdUri]);
+  }, [hackmdUri, pageId, revision?.body, mutateIsEnabledUnsavedWarning]);
 
 
   const penpalErrorOccuredHandler = useCallback((error) => {
   const penpalErrorOccuredHandler = useCallback((error) => {
     toastError(error.message);
     toastError(error.message);

+ 19 - 5
packages/app/src/components/PageList/PageListItemL.tsx

@@ -11,11 +11,10 @@ import { useTranslation } from 'next-i18next';
 import Link from 'next/link';
 import Link from 'next/link';
 import Clamp from 'react-multiline-clamp';
 import Clamp from 'react-multiline-clamp';
 import { CustomInput } from 'reactstrap';
 import { CustomInput } from 'reactstrap';
-import urljoin from 'url-join';
-
 
 
 import { ISelectable } from '~/client/interfaces/selectable-all';
 import { ISelectable } from '~/client/interfaces/selectable-all';
-import { bookmark, unbookmark } from '~/client/services/page-operation';
+import { unlink, bookmark, unbookmark } from '~/client/services/page-operation';
+import { toastError } from '~/client/util/apiNotification';
 import {
 import {
   IPageInfoAll, isIPageInfoForListing, isIPageInfoForEntity, IPageWithMeta, IPageInfoForListing,
   IPageInfoAll, isIPageInfoForListing, isIPageInfoForEntity, IPageWithMeta, IPageInfoForListing,
 } from '~/interfaces/page';
 } from '~/interfaces/page';
@@ -148,9 +147,24 @@ const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (pr
     openDeleteModal([pageToDelete], { onDeleted: onPageDeleted });
     openDeleteModal([pageToDelete], { onDeleted: onPageDeleted });
   }, [pageData, openDeleteModal, onPageDeleted]);
   }, [pageData, openDeleteModal, onPageDeleted]);
 
 
-  const revertMenuItemClickHandler = useCallback(() => {
+  const revertMenuItemClickHandler = useCallback(async() => {
     const { _id: pageId, path } = pageData;
     const { _id: pageId, path } = pageData;
-    openPutBackPageModal({ pageId, path }, { onPutBacked: onPagePutBacked });
+
+    const putBackedHandler = async(path) => {
+      try {
+        // pageData path should be `/trash/fuga` (`/trash` should be included to the prefix)
+        await unlink(pageData.path);
+      }
+      catch (err) {
+        toastError(err);
+      }
+
+      if (onPagePutBacked != null) {
+        // This path should be `/fuga` ( `/trash` is not included to the prefix)
+        onPagePutBacked(path);
+      }
+    };
+    openPutBackPageModal({ pageId, path }, { onPutBacked: putBackedHandler });
   }, [onPagePutBacked, openPutBackPageModal, pageData]);
   }, [onPagePutBacked, openPutBackPageModal, pageData]);
 
 
   const styleListGroupItem = (!isDeviceSmallerThanLg && onClickItem != null) ? 'list-group-item-action' : '';
   const styleListGroupItem = (!isDeviceSmallerThanLg && onClickItem != null) ? 'list-group-item-action' : '';

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

@@ -3,6 +3,7 @@ import React, { useCallback, useMemo } from 'react';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import * as ReactDOMServer from 'react-dom/server';
 import * as ReactDOMServer from 'react-dom/server';
 
 
+import { useIsGuestUser } from '~/stores/context';
 import { useEditingMarkdown, useIsConflict } from '~/stores/editor';
 import { useEditingMarkdown, useIsConflict } from '~/stores/editor';
 import {
 import {
   useHasDraftOnHackmd, useIsHackmdDraftUpdatingInRealtime, useRevisionIdHackmdSynced,
   useHasDraftOnHackmd, useIsHackmdDraftUpdatingInRealtime, useRevisionIdHackmdSynced,
@@ -31,6 +32,7 @@ export const PageStatusAlert = (): JSX.Element => {
   const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
   const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
   const { open: openConflictDiffModal } = useConflictDiffModal();
   const { open: openConflictDiffModal } = useConflictDiffModal();
   const { mutate: mutateEditorMode } = useEditorMode();
   const { mutate: mutateEditorMode } = useEditorMode();
+  const { data: isGuest } = useIsGuestUser();
 
 
   // store remote latest page data
   // store remote latest page data
   const { data: revisionIdHackmdSynced } = useRevisionIdHackmdSynced();
   const { data: revisionIdHackmdSynced } = useRevisionIdHackmdSynced();
@@ -151,7 +153,7 @@ export const PageStatusAlert = (): JSX.Element => {
     getContentsForDraftExistsAlert,
     getContentsForDraftExistsAlert,
   ]);
   ]);
 
 
-  if (alertComponentContents == null) { return <></> }
+  if (isGuest || alertComponentContents == null) { return <></> }
 
 
   const { additionalClasses, label, btn } = alertComponentContents;
   const { additionalClasses, label, btn } = alertComponentContents;
 
 

+ 17 - 14
packages/app/src/components/SearchPage.tsx

@@ -12,6 +12,7 @@ import { IFormattedSearchResult } from '~/interfaces/search';
 import { useIsSearchServiceReachable, useShowPageLimitationL } from '~/stores/context';
 import { useIsSearchServiceReachable, useShowPageLimitationL } from '~/stores/context';
 import { ISearchConditions, ISearchConfigurations, useSWRxSearch } from '~/stores/search';
 import { ISearchConditions, ISearchConfigurations, useSWRxSearch } from '~/stores/search';
 
 
+import { NotAvailableForGuest } from './NotAvailableForGuest';
 import PaginationWrapper from './PaginationWrapper';
 import PaginationWrapper from './PaginationWrapper';
 import { OperateAllControl } from './SearchPage/OperateAllControl';
 import { OperateAllControl } from './SearchPage/OperateAllControl';
 import SearchControl from './SearchPage/SearchControl';
 import SearchControl from './SearchPage/SearchControl';
@@ -183,21 +184,23 @@ export const SearchPage = (): JSX.Element => {
     const isDisabled = hitsCount === 0;
     const isDisabled = hitsCount === 0;
 
 
     return (
     return (
-      <OperateAllControl
-        ref={selectAllControlRef}
-        isCheckboxDisabled={isDisabled}
-        onCheckboxChanged={selectAllCheckboxChangedHandler}
-      >
-        <button
-          type="button"
-          className="btn btn-outline-danger text-nowrap border-0 px-2"
-          disabled={isDisabled}
-          onClick={deleteAllButtonClickedHandler}
+      <NotAvailableForGuest>
+        <OperateAllControl
+          ref={selectAllControlRef}
+          isCheckboxDisabled={isDisabled}
+          onCheckboxChanged={selectAllCheckboxChangedHandler}
         >
         >
-          <i className="icon-fw icon-trash"></i>
-          {t('search_result.delete_all_selected_page')}
-        </button>
-      </OperateAllControl>
+          <button
+            type="button"
+            className="btn btn-outline-danger text-nowrap border-0 px-2"
+            disabled={isDisabled}
+            onClick={deleteAllButtonClickedHandler}
+          >
+            <i className="icon-fw icon-trash"></i>
+            {t('search_result.delete_all_selected_page')}
+          </button>
+        </OperateAllControl>
+      </NotAvailableForGuest>
     );
     );
   }, [deleteAllButtonClickedHandler, hitsCount, selectAllCheckboxChangedHandler, t]);
   }, [deleteAllButtonClickedHandler, hitsCount, selectAllCheckboxChangedHandler, t]);
 
 

+ 14 - 11
packages/app/src/components/Sidebar.tsx

@@ -1,5 +1,5 @@
 import React, {
 import React, {
-  useCallback, useEffect, useRef, useState,
+  memo, useCallback, useEffect, useRef, useState,
 } from 'react';
 } from 'react';
 
 
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
@@ -22,12 +22,16 @@ import { StickyStretchableScrollerProps } from './StickyStretchableScroller';
 
 
 import styles from './Sidebar.module.scss';
 import styles from './Sidebar.module.scss';
 
 
+const StickyStretchableScroller = dynamic<StickyStretchableScrollerProps>(() => import('./StickyStretchableScroller')
+  .then(mod => mod.StickyStretchableScroller), { ssr: false });
+const SidebarContents = dynamic(() => import('./Sidebar/SidebarContents')
+  .then(mod => mod.SidebarContents), { ssr: false, loading: () => <SidebarSkeleton /> });
 
 
 const sidebarMinWidth = 240;
 const sidebarMinWidth = 240;
 const sidebarMinimizeWidth = 20;
 const sidebarMinimizeWidth = 20;
 const sidebarFixedWidthInDrawerMode = 320;
 const sidebarFixedWidthInDrawerMode = 320;
 
 
-const GlobalNavigation = () => {
+const GlobalNavigation = memo(() => {
   const { data: isDrawerMode } = useDrawerMode();
   const { data: isDrawerMode } = useDrawerMode();
   const { data: currentContents } = useCurrentSidebarContents();
   const { data: currentContents } = useCurrentSidebarContents();
   const { data: isCollapsed, mutate: mutateSidebarCollapsed } = useSidebarCollapsed();
   const { data: isCollapsed, mutate: mutateSidebarCollapsed } = useSidebarCollapsed();
@@ -54,13 +58,10 @@ const GlobalNavigation = () => {
 
 
   return <SidebarNav onItemSelected={itemSelectedHandler} />;
   return <SidebarNav onItemSelected={itemSelectedHandler} />;
 
 
-};
+});
+GlobalNavigation.displayName = 'GlobalNavigation';
 
 
-const SidebarContentsWrapper = () => {
-  const StickyStretchableScroller = dynamic<StickyStretchableScrollerProps>(() => import('./StickyStretchableScroller')
-    .then(mod => mod.StickyStretchableScroller), { ssr: false, loading: () => <SidebarSkeleton /> });
-  const SidebarContents = dynamic(() => import('./Sidebar/SidebarContents')
-    .then(mod => mod.SidebarContents), { ssr: false, loading: () => <SidebarSkeleton /> });
+const SidebarContentsWrapper = memo(() => {
   const { mutate: mutateSidebarScroller } = useSidebarScrollerRef();
   const { mutate: mutateSidebarScroller } = useSidebarScrollerRef();
 
 
   const calcViewHeight = useCallback(() => {
   const calcViewHeight = useCallback(() => {
@@ -85,10 +86,11 @@ const SidebarContentsWrapper = () => {
       <DrawerToggler iconClass="icon-arrow-left" />
       <DrawerToggler iconClass="icon-arrow-left" />
     </>
     </>
   );
   );
-};
+});
+SidebarContentsWrapper.displayName = 'SidebarContentsWrapper';
 
 
 
 
-const Sidebar = (): JSX.Element => {
+const Sidebar = memo((): JSX.Element => {
 
 
   const { data: isDrawerMode } = useDrawerMode();
   const { data: isDrawerMode } = useDrawerMode();
   const { data: isDrawerOpened, mutate: mutateDrawerOpened } = useDrawerOpened();
   const { data: isDrawerOpened, mutate: mutateDrawerOpened } = useDrawerOpened();
@@ -354,6 +356,7 @@ const Sidebar = (): JSX.Element => {
     </>
     </>
   );
   );
 
 
-};
+});
+Sidebar.displayName = 'Sidebar';
 
 
 export default Sidebar;
 export default Sidebar;

+ 1 - 1
packages/app/src/components/Sidebar/CustomSidebar.tsx

@@ -47,7 +47,7 @@ const CustomSidebar: FC = () => {
       <div className="grw-sidebar-content-header py-3 d-flex">
       <div className="grw-sidebar-content-header py-3 d-flex">
         <h3 className="mb-0">
         <h3 className="mb-0">
           {t('CustomSidebar')}
           {t('CustomSidebar')}
-          <Link href="/Sidebar"><a className="h6 ml-2"><i className="icon-pencil"></i></a></Link>
+          <Link href="/Sidebar#edit"><a className="h6 ml-2"><i className="icon-pencil"></i></a></Link>
         </h3>
         </h3>
         <SidebarHeaderReloadButton onClick={() => mutate()} />
         <SidebarHeaderReloadButton onClick={() => mutate()} />
       </div>
       </div>

+ 1 - 1
packages/app/src/components/Sidebar/PageTree/PrivateLegacyPagesLink.tsx

@@ -10,7 +10,7 @@ export const PrivateLegacyPagesLink: FC = memo(() => {
   return (
   return (
     <Link href="/_private-legacy-pages" prefetch={false}>
     <Link href="/_private-legacy-pages" prefetch={false}>
       <a className="h5 grw-private-legacy-pages-anchor text-decoration-none">
       <a className="h5 grw-private-legacy-pages-anchor text-decoration-none">
-        <i className="icon-drawer mr-2"></i> {t('pagetree.private_legacy_pages')}
+        <i className="icon-drawer mr-2"></i> {t('private_legacy_pages.title')}
       </a>
       </a>
     </Link>
     </Link>
   );
   );

+ 1 - 1
packages/app/src/components/Sidebar/RecentChanges.module.scss

@@ -29,7 +29,7 @@
 
 
   .grw-recent-changes-skeleton-date {
   .grw-recent-changes-skeleton-date {
     @include grw-skeleton-text($font-size:10px, $line-height:12px);
     @include grw-skeleton-text($font-size:10px, $line-height:12px);
-    width: 90px;
+    width: 80px;
   }
   }
 
 
   .grw-recent-changes-item-lower {
   .grw-recent-changes-item-lower {

+ 4 - 4
packages/app/src/components/Sidebar/SidebarContents.tsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { memo } from 'react';
 
 
 import { SidebarContentsType } from '~/interfaces/ui';
 import { SidebarContentsType } from '~/interfaces/ui';
 import { useCurrentSidebarContents } from '~/stores/ui';
 import { useCurrentSidebarContents } from '~/stores/ui';
@@ -8,7 +8,7 @@ import PageTree from './PageTree';
 import RecentChanges from './RecentChanges';
 import RecentChanges from './RecentChanges';
 import Tag from './Tag';
 import Tag from './Tag';
 
 
-export const SidebarContents = (): JSX.Element => {
+export const SidebarContents = memo(() => {
   const { data: currentSidebarContents } = useCurrentSidebarContents();
   const { data: currentSidebarContents } = useCurrentSidebarContents();
 
 
   let Contents;
   let Contents;
@@ -29,5 +29,5 @@ export const SidebarContents = (): JSX.Element => {
   return (
   return (
     <Contents />
     <Contents />
   );
   );
-
-};
+});
+SidebarContents.displayName = 'SidebarContents';

+ 23 - 24
packages/app/src/components/Sidebar/Skeleton/SidebarSkeleton.tsx

@@ -1,6 +1,7 @@
-import React from 'react';
+import React, { memo } from 'react';
+
+import { useTranslation } from 'next-i18next';
 
 
-import { Skeleton } from '~/components/Skeleton';
 import { SidebarContentsType } from '~/interfaces/ui';
 import { SidebarContentsType } from '~/interfaces/ui';
 import { useCurrentSidebarContents } from '~/stores/ui';
 import { useCurrentSidebarContents } from '~/stores/ui';
 
 
@@ -9,42 +10,40 @@ import PageTreeContentSkeleton from './PageTreeContentSkeleton';
 import RecentChangesContentSkeleton from './RecentChangesContentSkeleton';
 import RecentChangesContentSkeleton from './RecentChangesContentSkeleton';
 import TagContentSkeleton from './TagContentSkeleton';
 import TagContentSkeleton from './TagContentSkeleton';
 
 
-import styles from './SidebarSkeleton.module.scss';
-
-export const SidebarHeaderSkeleton = (): JSX.Element => {
-  return (
-    <div className="grw-sidebar-content-header py-3">
-      <Skeleton additionalClass={styles['grw-sidebar-content-header-skeleton']} />
-    </div>
-  );
-};
-
-export const SidebarSkeleton = (): JSX.Element => {
-
+export const SidebarSkeleton = memo(() => {
+  const { t } = useTranslation();
   const { data: currentSidebarContents } = useCurrentSidebarContents();
   const { data: currentSidebarContents } = useCurrentSidebarContents();
 
 
-  let SidebarContentSkeleton: () => JSX.Element;
+  let Contents: () => JSX.Element;
+  let title: string;
   switch (currentSidebarContents) {
   switch (currentSidebarContents) {
 
 
-    case SidebarContentsType.TAG:
-      SidebarContentSkeleton = TagContentSkeleton;
-      break;
     case SidebarContentsType.RECENT:
     case SidebarContentsType.RECENT:
-      SidebarContentSkeleton = RecentChangesContentSkeleton;
+      Contents = RecentChangesContentSkeleton;
+      title = t('Recent Changes');
       break;
       break;
     case SidebarContentsType.CUSTOM:
     case SidebarContentsType.CUSTOM:
-      SidebarContentSkeleton = CustomSidebarContentSkeleton;
+      Contents = CustomSidebarContentSkeleton;
+      title = t('CustomSidebar');
+      break;
+    case SidebarContentsType.TAG:
+      Contents = TagContentSkeleton;
+      title = t('Tags');
       break;
       break;
     case SidebarContentsType.TREE:
     case SidebarContentsType.TREE:
     default:
     default:
-      SidebarContentSkeleton = PageTreeContentSkeleton;
+      Contents = PageTreeContentSkeleton;
+      title = t('Page Tree');
       break;
       break;
   }
   }
 
 
   return (
   return (
     <div className={currentSidebarContents === SidebarContentsType.TAG ? 'px-4' : 'px-3'}>
     <div className={currentSidebarContents === SidebarContentsType.TAG ? 'px-4' : 'px-3'}>
-      <SidebarHeaderSkeleton />
-      <SidebarContentSkeleton />
+      <div className="grw-sidebar-content-header py-3">
+        <h3 className="mb-0">{title}</h3>
+      </div>
+      <Contents />
     </div>
     </div>
   );
   );
-};
+});
+SidebarSkeleton.displayName = 'SidebarSkeleton';

+ 4 - 1
packages/app/src/components/Sidebar/Skeleton/TagContentSkeleton.tsx

@@ -1,5 +1,7 @@
 import React from 'react';
 import React from 'react';
 
 
+import { useTranslation } from 'next-i18next';
+
 import { Skeleton } from '~/components/Skeleton';
 import { Skeleton } from '~/components/Skeleton';
 
 
 import styles from '../Tag.module.scss';
 import styles from '../Tag.module.scss';
@@ -11,10 +13,11 @@ export const TagListSkeleton = (): JSX.Element => {
 };
 };
 
 
 const TagContentSkeleton = (): JSX.Element => {
 const TagContentSkeleton = (): JSX.Element => {
+  const { t } = useTranslation('');
 
 
   return (
   return (
     <>
     <>
-      <Skeleton additionalClass={`${styles['grw-tag-skeleton-h3']} my-3`} />
+      <h3 className="my-3">{t('tag_list')}</h3>
       <TagListSkeleton />
       <TagListSkeleton />
     </>
     </>
   );
   );

+ 0 - 5
packages/app/src/components/Sidebar/Tag.module.scss

@@ -1,10 +1,5 @@
 @use '~/styles/mixins' as *;
 @use '~/styles/mixins' as *;
 
 
-.grw-tag-skeleton-h3 {
-  @include grw-skeleton-h3;
-  max-width: 120px;
-}
-
 .grw-tag-list-skeleton {
 .grw-tag-list-skeleton {
   height: 90px;
   height: 90px;
 }
 }

+ 2 - 3
packages/app/src/components/StickyStretchableScroller.tsx

@@ -1,5 +1,5 @@
 import React, {
 import React, {
-  useEffect, useCallback, ReactNode, useRef, useState, useMemo, RefObject,
+  useEffect, useCallback, FC, useRef, useState, useMemo, RefObject,
 } from 'react';
 } from 'react';
 
 
 import SimpleBar from 'simplebar-react';
 import SimpleBar from 'simplebar-react';
@@ -15,7 +15,6 @@ export type StickyStretchableScrollerProps = {
   stickyElemSelector: string,
   stickyElemSelector: string,
   simplebarRef?: (ref: RefObject<SimpleBar>) => void,
   simplebarRef?: (ref: RefObject<SimpleBar>) => void,
   calcViewHeight?: (scrollElement: HTMLElement) => number,
   calcViewHeight?: (scrollElement: HTMLElement) => number,
-  children?: ReactNode,
 }
 }
 
 
 /**
 /**
@@ -40,7 +39,7 @@ export type StickyStretchableScrollerProps = {
     </StickyStretchableScroller>
     </StickyStretchableScroller>
   );
   );
  */
  */
-export const StickyStretchableScroller = (props: StickyStretchableScrollerProps): JSX.Element => {
+export const StickyStretchableScroller: FC<StickyStretchableScrollerProps> = (props) => {
 
 
   const {
   const {
     children, stickyElemSelector, calcViewHeight, simplebarRef: setSimplebarRef,
     children, stickyElemSelector, calcViewHeight, simplebarRef: setSimplebarRef,

+ 2 - 2
packages/app/src/components/TableOfContents.tsx

@@ -1,14 +1,13 @@
 import React, { useCallback } from 'react';
 import React, { useCallback } from 'react';
 
 
 import { pagePathUtils } from '@growi/core';
 import { pagePathUtils } from '@growi/core';
+import dynamic from 'next/dynamic';
 import ReactMarkdown from 'react-markdown';
 import ReactMarkdown from 'react-markdown';
 
 
 import { useCurrentPagePath } from '~/stores/page';
 import { useCurrentPagePath } from '~/stores/page';
 import { useTocOptions } from '~/stores/renderer';
 import { useTocOptions } from '~/stores/renderer';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-import { StickyStretchableScroller } from './StickyStretchableScroller';
-
 import styles from './TableOfContents.module.scss';
 import styles from './TableOfContents.module.scss';
 
 
 const { isUserPage: _isUserPage } = pagePathUtils;
 const { isUserPage: _isUserPage } = pagePathUtils;
@@ -17,6 +16,7 @@ const { isUserPage: _isUserPage } = pagePathUtils;
 const logger = loggerFactory('growi:TableOfContents');
 const logger = loggerFactory('growi:TableOfContents');
 
 
 const TableOfContents = (): JSX.Element => {
 const TableOfContents = (): JSX.Element => {
+  const StickyStretchableScroller = dynamic(() => import('./StickyStretchableScroller').then(mod => mod.StickyStretchableScroller), { ssr: false });
 
 
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPagePath } = useCurrentPagePath();
 
 

+ 1 - 1
packages/app/src/components/User/UserInfo.tsx

@@ -19,7 +19,7 @@ export const UserInfo = (props: UserInfoProps): JSX.Element => {
   }
   }
 
 
   return (
   return (
-    <div className={`${styles['grw-users-info']} grw-users-info d-flex align-items-center d-edit-none mb-5 pb-3 border-bottom`}>
+    <div className={`${styles['grw-users-info']} grw-users-info d-flex align-items-center d-edit-none mb-5 pb-3 border-bottom`} data-testid="grw-users-info">
       <UserPicture user={author} />
       <UserPicture user={author} />
       <div className="users-meta">
       <div className="users-meta">
         <h1 className="user-page-name">
         <h1 className="user-page-name">

+ 3 - 27
packages/app/src/interfaces/activity.ts

@@ -13,10 +13,8 @@ const ACTION_USER_LOGIN_WITH_LOCAL = 'USER_LOGIN_WITH_LOCAL';
 const ACTION_USER_LOGIN_WITH_LDAP = 'USER_LOGIN_WITH_LDAP';
 const ACTION_USER_LOGIN_WITH_LDAP = 'USER_LOGIN_WITH_LDAP';
 const ACTION_USER_LOGIN_WITH_GOOGLE = 'USER_LOGIN_WITH_GOOGLE';
 const ACTION_USER_LOGIN_WITH_GOOGLE = 'USER_LOGIN_WITH_GOOGLE';
 const ACTION_USER_LOGIN_WITH_GITHUB = 'USER_LOGIN_WITH_GITHUB';
 const ACTION_USER_LOGIN_WITH_GITHUB = 'USER_LOGIN_WITH_GITHUB';
-const ACTION_USER_LOGIN_WITH_TWITTER = 'USER_LOGIN_WITH_TWITTER';
 const ACTION_USER_LOGIN_WITH_OIDC = 'USER_LOGIN_WITH_OIDC';
 const ACTION_USER_LOGIN_WITH_OIDC = 'USER_LOGIN_WITH_OIDC';
 const ACTION_USER_LOGIN_WITH_SAML = 'USER_LOGIN_WITH_SAML';
 const ACTION_USER_LOGIN_WITH_SAML = 'USER_LOGIN_WITH_SAML';
-const ACTION_USER_LOGIN_WITH_BASIC = 'USER_LOGIN_WITH_BASIC';
 const ACTION_USER_LOGIN_FAILURE = 'USER_LOGIN_FAILURE';
 const ACTION_USER_LOGIN_FAILURE = 'USER_LOGIN_FAILURE';
 const ACTION_USER_LOGOUT = 'USER_LOGOUT';
 const ACTION_USER_LOGOUT = 'USER_LOGOUT';
 const ACTION_USER_FOGOT_PASSWORD = 'USER_FOGOT_PASSWORD';
 const ACTION_USER_FOGOT_PASSWORD = 'USER_FOGOT_PASSWORD';
@@ -94,18 +92,12 @@ const ACTION_ADMIN_AUTH_SAML_UPDATE = 'ADMIN_AUTH_SAML_UPDATE';
 const ACTION_ADMIN_AUTH_OIDC_ENABLED = 'ADMIN_AUTH_OIDC_ENABLED';
 const ACTION_ADMIN_AUTH_OIDC_ENABLED = 'ADMIN_AUTH_OIDC_ENABLED';
 const ACTION_ADMIN_AUTH_OIDC_DISABLED = 'ADMIN_AUTH_OIDC_DISABLED';
 const ACTION_ADMIN_AUTH_OIDC_DISABLED = 'ADMIN_AUTH_OIDC_DISABLED';
 const ACTION_ADMIN_AUTH_OIDC_UPDATE = 'ADMIN_AUTH_OIDC_UPDATE';
 const ACTION_ADMIN_AUTH_OIDC_UPDATE = 'ADMIN_AUTH_OIDC_UPDATE';
-const ACTION_ADMIN_AUTH_BASIC_ENABLED = 'ADMIN_AUTH_BASIC_ENABLED';
-const ACTION_ADMIN_AUTH_BASIC_DISABLED = 'ADMIN_AUTH_BASIC_DISABLED';
-const ACTION_ADMIN_AUTH_BASIC_UPDATE = 'ADMIN_AUTH_BASIC_UPDATE';
 const ACTION_ADMIN_AUTH_GOOGLE_ENABLED = 'ADMIN_AUTH_GOOGLE_ENABLED';
 const ACTION_ADMIN_AUTH_GOOGLE_ENABLED = 'ADMIN_AUTH_GOOGLE_ENABLED';
 const ACTION_ADMIN_AUTH_GOOGLE_DISABLED = 'ADMIN_AUTH_GOOGLE_DISABLED';
 const ACTION_ADMIN_AUTH_GOOGLE_DISABLED = 'ADMIN_AUTH_GOOGLE_DISABLED';
 const ACTION_ADMIN_AUTH_GOOGLE_UPDATE = 'ADMIN_AUTH_GOOGLE_UPDATE';
 const ACTION_ADMIN_AUTH_GOOGLE_UPDATE = 'ADMIN_AUTH_GOOGLE_UPDATE';
 const ACTION_ADMIN_AUTH_GITHUB_ENABLED = 'ADMIN_AUTH_GITHUB_ENABLED';
 const ACTION_ADMIN_AUTH_GITHUB_ENABLED = 'ADMIN_AUTH_GITHUB_ENABLED';
 const ACTION_ADMIN_AUTH_GITHUB_DISABLED = 'ADMIN_AUTH_GITHUB_DISABLED';
 const ACTION_ADMIN_AUTH_GITHUB_DISABLED = 'ADMIN_AUTH_GITHUB_DISABLED';
 const ACTION_ADMIN_AUTH_GITHUB_UPDATE = 'ADMIN_AUTH_GITHUB_UPDATE';
 const ACTION_ADMIN_AUTH_GITHUB_UPDATE = 'ADMIN_AUTH_GITHUB_UPDATE';
-const ACTION_ADMIN_AUTH_TWITTER_ENABLED = 'ADMIN_AUTH_TWITTER_ENABLED';
-const ACTION_ADMIN_AUTH_TWITTER_DISABLED = 'ADMIN_AUTH_TWITTER_DISABLED';
-const ACTION_ADMIN_AUTH_TWITTER_UPDATE = 'ADMIN_AUTH_TWITTER_UPDATE';
 const ACTION_ADMIN_MARKDOWN_LINE_BREAK_UPDATE = 'ADMIN_MARKDOWN_LINE_BREAK_UPDATE';
 const ACTION_ADMIN_MARKDOWN_LINE_BREAK_UPDATE = 'ADMIN_MARKDOWN_LINE_BREAK_UPDATE';
 const ACTION_ADMIN_MARKDOWN_INDENT_UPDATE = 'ADMIN_MARKDOWN_INDENT_UPDATE';
 const ACTION_ADMIN_MARKDOWN_INDENT_UPDATE = 'ADMIN_MARKDOWN_INDENT_UPDATE';
 const ACTION_ADMIN_MARKDOWN_PRESENTATION_UPDATE = 'ADMIN_MARKDOWN_PRESENTATION_UPDATE';
 const ACTION_ADMIN_MARKDOWN_PRESENTATION_UPDATE = 'ADMIN_MARKDOWN_PRESENTATION_UPDATE';
@@ -116,7 +108,7 @@ const ACTION_ADMIN_SIDEBAR_UPDATE = 'ADMIN_SIDEBAR_UPDATE';
 const ACTION_ADMIN_FUNCTION_UPDATE = 'ADMIN_FUNCTION_UPDATE';
 const ACTION_ADMIN_FUNCTION_UPDATE = 'ADMIN_FUNCTION_UPDATE';
 const ACTION_ADMIN_CODE_HIGHLIGHT_UPDATE = 'ADMIN_CODE_HIGHLIGHT_UPDATE';
 const ACTION_ADMIN_CODE_HIGHLIGHT_UPDATE = 'ADMIN_CODE_HIGHLIGHT_UPDATE';
 const ACTION_ADMIN_CUSTOM_TITLE_UPDATE = 'ADMIN_CUSTOM_TITLE_UPDATE';
 const ACTION_ADMIN_CUSTOM_TITLE_UPDATE = 'ADMIN_CUSTOM_TITLE_UPDATE';
-const ACTION_ADMIN_CUSTOM_HTML_HEADER_UPDATE = 'ADMIN_CUSTOM_HTML_HEADER_UPDATE';
+const ACTION_ADMIN_CUSTOM_NOSCRIPT_UPDATE = 'ADMIN_CUSTOM_NOSCRIPT_UPDATE';
 const ACTION_ADMIN_CUSTOM_CSS_UPDATE = 'ADMIN_CUSTOM_CSS_UPDATE';
 const ACTION_ADMIN_CUSTOM_CSS_UPDATE = 'ADMIN_CUSTOM_CSS_UPDATE';
 const ACTION_ADMIN_CUSTOM_SCRIPT_UPDATE = 'ADMIN_CUSTOM_SCRIPT_UPDATE';
 const ACTION_ADMIN_CUSTOM_SCRIPT_UPDATE = 'ADMIN_CUSTOM_SCRIPT_UPDATE';
 const ACTION_ADMIN_ARCHIVE_DATA_UPLOAD = 'ADMIN_ARCHIVE_DATA_UPLOAD';
 const ACTION_ADMIN_ARCHIVE_DATA_UPLOAD = 'ADMIN_ARCHIVE_DATA_UPLOAD';
@@ -196,10 +188,8 @@ export const SupportedAction = {
   ACTION_USER_LOGIN_WITH_LDAP,
   ACTION_USER_LOGIN_WITH_LDAP,
   ACTION_USER_LOGIN_WITH_GOOGLE,
   ACTION_USER_LOGIN_WITH_GOOGLE,
   ACTION_USER_LOGIN_WITH_GITHUB,
   ACTION_USER_LOGIN_WITH_GITHUB,
-  ACTION_USER_LOGIN_WITH_TWITTER,
   ACTION_USER_LOGIN_WITH_OIDC,
   ACTION_USER_LOGIN_WITH_OIDC,
   ACTION_USER_LOGIN_WITH_SAML,
   ACTION_USER_LOGIN_WITH_SAML,
-  ACTION_USER_LOGIN_WITH_BASIC,
   ACTION_USER_LOGIN_FAILURE,
   ACTION_USER_LOGIN_FAILURE,
   ACTION_USER_LOGOUT,
   ACTION_USER_LOGOUT,
   ACTION_USER_FOGOT_PASSWORD,
   ACTION_USER_FOGOT_PASSWORD,
@@ -277,18 +267,12 @@ export const SupportedAction = {
   ACTION_ADMIN_AUTH_OIDC_ENABLED,
   ACTION_ADMIN_AUTH_OIDC_ENABLED,
   ACTION_ADMIN_AUTH_OIDC_DISABLED,
   ACTION_ADMIN_AUTH_OIDC_DISABLED,
   ACTION_ADMIN_AUTH_OIDC_UPDATE,
   ACTION_ADMIN_AUTH_OIDC_UPDATE,
-  ACTION_ADMIN_AUTH_BASIC_ENABLED,
-  ACTION_ADMIN_AUTH_BASIC_DISABLED,
-  ACTION_ADMIN_AUTH_BASIC_UPDATE,
   ACTION_ADMIN_AUTH_GOOGLE_ENABLED,
   ACTION_ADMIN_AUTH_GOOGLE_ENABLED,
   ACTION_ADMIN_AUTH_GOOGLE_DISABLED,
   ACTION_ADMIN_AUTH_GOOGLE_DISABLED,
   ACTION_ADMIN_AUTH_GOOGLE_UPDATE,
   ACTION_ADMIN_AUTH_GOOGLE_UPDATE,
   ACTION_ADMIN_AUTH_GITHUB_ENABLED,
   ACTION_ADMIN_AUTH_GITHUB_ENABLED,
   ACTION_ADMIN_AUTH_GITHUB_DISABLED,
   ACTION_ADMIN_AUTH_GITHUB_DISABLED,
   ACTION_ADMIN_AUTH_GITHUB_UPDATE,
   ACTION_ADMIN_AUTH_GITHUB_UPDATE,
-  ACTION_ADMIN_AUTH_TWITTER_ENABLED,
-  ACTION_ADMIN_AUTH_TWITTER_DISABLED,
-  ACTION_ADMIN_AUTH_TWITTER_UPDATE,
   ACTION_ADMIN_MARKDOWN_LINE_BREAK_UPDATE,
   ACTION_ADMIN_MARKDOWN_LINE_BREAK_UPDATE,
   ACTION_ADMIN_MARKDOWN_INDENT_UPDATE,
   ACTION_ADMIN_MARKDOWN_INDENT_UPDATE,
   ACTION_ADMIN_MARKDOWN_PRESENTATION_UPDATE,
   ACTION_ADMIN_MARKDOWN_PRESENTATION_UPDATE,
@@ -299,7 +283,7 @@ export const SupportedAction = {
   ACTION_ADMIN_FUNCTION_UPDATE,
   ACTION_ADMIN_FUNCTION_UPDATE,
   ACTION_ADMIN_CODE_HIGHLIGHT_UPDATE,
   ACTION_ADMIN_CODE_HIGHLIGHT_UPDATE,
   ACTION_ADMIN_CUSTOM_TITLE_UPDATE,
   ACTION_ADMIN_CUSTOM_TITLE_UPDATE,
-  ACTION_ADMIN_CUSTOM_HTML_HEADER_UPDATE,
+  ACTION_ADMIN_CUSTOM_NOSCRIPT_UPDATE,
   ACTION_ADMIN_CUSTOM_CSS_UPDATE,
   ACTION_ADMIN_CUSTOM_CSS_UPDATE,
   ACTION_ADMIN_CUSTOM_SCRIPT_UPDATE,
   ACTION_ADMIN_CUSTOM_SCRIPT_UPDATE,
   ACTION_ADMIN_ARCHIVE_DATA_UPLOAD,
   ACTION_ADMIN_ARCHIVE_DATA_UPLOAD,
@@ -380,10 +364,8 @@ export const SmallActionGroup = {
   ACTION_USER_LOGIN_WITH_LDAP,
   ACTION_USER_LOGIN_WITH_LDAP,
   ACTION_USER_LOGIN_WITH_GOOGLE,
   ACTION_USER_LOGIN_WITH_GOOGLE,
   ACTION_USER_LOGIN_WITH_GITHUB,
   ACTION_USER_LOGIN_WITH_GITHUB,
-  ACTION_USER_LOGIN_WITH_TWITTER,
   ACTION_USER_LOGIN_WITH_OIDC,
   ACTION_USER_LOGIN_WITH_OIDC,
   ACTION_USER_LOGIN_WITH_SAML,
   ACTION_USER_LOGIN_WITH_SAML,
-  ACTION_USER_LOGIN_WITH_BASIC,
   ACTION_USER_LOGIN_FAILURE,
   ACTION_USER_LOGIN_FAILURE,
   ACTION_USER_LOGOUT,
   ACTION_USER_LOGOUT,
   ACTION_PAGE_CREATE,
   ACTION_PAGE_CREATE,
@@ -468,18 +450,12 @@ export const LargeActionGroup = {
   ACTION_ADMIN_AUTH_OIDC_ENABLED,
   ACTION_ADMIN_AUTH_OIDC_ENABLED,
   ACTION_ADMIN_AUTH_OIDC_DISABLED,
   ACTION_ADMIN_AUTH_OIDC_DISABLED,
   ACTION_ADMIN_AUTH_OIDC_UPDATE,
   ACTION_ADMIN_AUTH_OIDC_UPDATE,
-  ACTION_ADMIN_AUTH_BASIC_ENABLED,
-  ACTION_ADMIN_AUTH_BASIC_DISABLED,
-  ACTION_ADMIN_AUTH_BASIC_UPDATE,
   ACTION_ADMIN_AUTH_GOOGLE_ENABLED,
   ACTION_ADMIN_AUTH_GOOGLE_ENABLED,
   ACTION_ADMIN_AUTH_GOOGLE_DISABLED,
   ACTION_ADMIN_AUTH_GOOGLE_DISABLED,
   ACTION_ADMIN_AUTH_GOOGLE_UPDATE,
   ACTION_ADMIN_AUTH_GOOGLE_UPDATE,
   ACTION_ADMIN_AUTH_GITHUB_ENABLED,
   ACTION_ADMIN_AUTH_GITHUB_ENABLED,
   ACTION_ADMIN_AUTH_GITHUB_DISABLED,
   ACTION_ADMIN_AUTH_GITHUB_DISABLED,
   ACTION_ADMIN_AUTH_GITHUB_UPDATE,
   ACTION_ADMIN_AUTH_GITHUB_UPDATE,
-  ACTION_ADMIN_AUTH_TWITTER_ENABLED,
-  ACTION_ADMIN_AUTH_TWITTER_DISABLED,
-  ACTION_ADMIN_AUTH_TWITTER_UPDATE,
   ACTION_ADMIN_MARKDOWN_LINE_BREAK_UPDATE,
   ACTION_ADMIN_MARKDOWN_LINE_BREAK_UPDATE,
   ACTION_ADMIN_MARKDOWN_INDENT_UPDATE,
   ACTION_ADMIN_MARKDOWN_INDENT_UPDATE,
   ACTION_ADMIN_MARKDOWN_PRESENTATION_UPDATE,
   ACTION_ADMIN_MARKDOWN_PRESENTATION_UPDATE,
@@ -490,7 +466,7 @@ export const LargeActionGroup = {
   ACTION_ADMIN_FUNCTION_UPDATE,
   ACTION_ADMIN_FUNCTION_UPDATE,
   ACTION_ADMIN_CODE_HIGHLIGHT_UPDATE,
   ACTION_ADMIN_CODE_HIGHLIGHT_UPDATE,
   ACTION_ADMIN_CUSTOM_TITLE_UPDATE,
   ACTION_ADMIN_CUSTOM_TITLE_UPDATE,
-  ACTION_ADMIN_CUSTOM_HTML_HEADER_UPDATE,
+  ACTION_ADMIN_CUSTOM_NOSCRIPT_UPDATE,
   ACTION_ADMIN_CUSTOM_CSS_UPDATE,
   ACTION_ADMIN_CUSTOM_CSS_UPDATE,
   ACTION_ADMIN_CUSTOM_SCRIPT_UPDATE,
   ACTION_ADMIN_CUSTOM_SCRIPT_UPDATE,
   ACTION_ADMIN_ARCHIVE_DATA_UPLOAD,
   ACTION_ADMIN_ARCHIVE_DATA_UPLOAD,

+ 0 - 1
packages/app/src/interfaces/editor-settings.ts

@@ -24,7 +24,6 @@ export interface IEditorSettings {
   theme: undefined | string,
   theme: undefined | string,
   keymapMode: undefined | KeyMapMode,
   keymapMode: undefined | KeyMapMode,
   styleActiveLine: boolean,
   styleActiveLine: boolean,
-  renderDrawioInRealtime: boolean,
   autoFormatMarkdownTable: boolean,
   autoFormatMarkdownTable: boolean,
   textlintSettings: undefined | ITextlintSettings;
   textlintSettings: undefined | ITextlintSettings;
 }
 }

+ 0 - 1
packages/app/src/migrations/20180927102719-init-serverurl.js

@@ -40,7 +40,6 @@ module.exports = {
       $or: [
       $or: [
         { key: 'security:passport-github:callbackUrl' },
         { key: 'security:passport-github:callbackUrl' },
         { key: 'security:passport-google:callbackUrl' },
         { key: 'security:passport-google:callbackUrl' },
-        { key: 'security:passport-twitter:callbackUrl' },
         { key: 'security:passport-saml:callbackUrl' },
         { key: 'security:passport-saml:callbackUrl' },
       ],
       ],
     });
     });

+ 25 - 0
packages/app/src/migrations/20221219011829-remove-basic-auth-related-config.js

@@ -0,0 +1,25 @@
+// eslint-disable-next-line import/no-named-as-default
+import Config from '~/server/models/config';
+import { getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
+import loggerFactory from '~/utils/logger';
+
+
+const logger = loggerFactory('growi:remove-basic-auth-related-config');
+
+const mongoose = require('mongoose');
+
+module.exports = {
+  async up() {
+    logger.info('Apply migration');
+    mongoose.connect(getMongoUri(), mongoOptions);
+
+    await Config.findOneAndDelete({ key: 'security:passport-basic:isEnabled' });
+    await Config.findOneAndDelete({ key: 'security:passport-basic:isSameUsernameTreatedAsIdenticalUser' });
+
+    logger.info('Migration has successfully applied');
+  },
+
+  async down() {
+    // No rollback
+  },
+};

+ 78 - 83
packages/app/src/pages/[[...path]].page.tsx

@@ -11,7 +11,7 @@ import type {
 } from '@growi/core';
 } from '@growi/core';
 import ExtensibleCustomError from 'extensible-custom-error';
 import ExtensibleCustomError from 'extensible-custom-error';
 import {
 import {
-  NextPage, GetServerSideProps, GetServerSidePropsContext,
+  GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
 } from 'next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
@@ -19,6 +19,7 @@ import Head from 'next/head';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
 import superjson from 'superjson';
 import superjson from 'superjson';
 
 
+import { useCurrentGrowiLayoutFluidClassName } from '~/client/services/layout';
 import { Comments } from '~/components/Comments';
 import { Comments } from '~/components/Comments';
 import { PageAlerts } from '~/components/PageAlert/PageAlerts';
 import { PageAlerts } from '~/components/PageAlert/PageAlerts';
 // import { useTranslation } from '~/i18n';
 // import { useTranslation } from '~/i18n';
@@ -42,7 +43,7 @@ import { useSWRxCurrentPage, useSWRxIsGrantNormalized } from '~/stores/page';
 import { useRedirectFrom } from '~/stores/page-redirect';
 import { useRedirectFrom } from '~/stores/page-redirect';
 import { useRemoteRevisionId } from '~/stores/remote-latest-page';
 import { useRemoteRevisionId } from '~/stores/remote-latest-page';
 import {
 import {
-  useEditorMode, useSelectedGrant,
+  useSelectedGrant,
   usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed, useCurrentSidebarContents, useCurrentProductNavWidth,
   usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed, useCurrentSidebarContents, useCurrentProductNavWidth,
 } from '~/stores/ui';
 } from '~/stores/ui';
 import { useSetupGlobalSocket, useSetupGlobalSocketForPage } from '~/stores/websocket';
 import { useSetupGlobalSocket, useSetupGlobalSocketForPage } from '~/stores/websocket';
@@ -53,7 +54,7 @@ import loggerFactory from '~/utils/logger';
 // import GrowiSubNavigation from '../client/js/components/Navbar/GrowiSubNavigation';
 // import GrowiSubNavigation from '../client/js/components/Navbar/GrowiSubNavigation';
 // import GrowiSubNavigationSwitcher from '../client/js/components/Navbar/GrowiSubNavigationSwitcher';
 // import GrowiSubNavigationSwitcher from '../client/js/components/Navbar/GrowiSubNavigationSwitcher';
 import { DescendantsPageListModal } from '../components/DescendantsPageListModal';
 import { DescendantsPageListModal } from '../components/DescendantsPageListModal';
-import { BasicLayout } from '../components/Layout/BasicLayout';
+import { BasicLayoutWithEditorMode } from '../components/Layout/BasicLayout';
 import GrowiContextualSubNavigation from '../components/Navbar/GrowiContextualSubNavigation';
 import GrowiContextualSubNavigation from '../components/Navbar/GrowiContextualSubNavigation';
 import DisplaySwitcher from '../components/Page/DisplaySwitcher';
 import DisplaySwitcher from '../components/Page/DisplaySwitcher';
 // import { serializeUserSecurely } from '../server/models/serializers/user-serializer';
 // import { serializeUserSecurely } from '../server/models/serializers/user-serializer';
@@ -71,10 +72,10 @@ import {
   useEditorConfig, useIsAllReplyShown, useIsUploadableFile, useIsUploadableImage, useCustomizedLogoSrc, useIsContainerFluid,
   useEditorConfig, useIsAllReplyShown, useIsUploadableFile, useIsUploadableImage, useCustomizedLogoSrc, useIsContainerFluid,
 } from '../stores/context';
 } from '../stores/context';
 
 
+import { NextPageWithLayout } from './_app.page';
 import {
 import {
-  CommonProps, getNextI18NextConfig, getServerSideCommonProps, useCustomTitle,
+  CommonProps, getNextI18NextConfig, getServerSideCommonProps, generateCustomTitle,
 } from './utils/commons';
 } from './utils/commons';
-// import { useCurrentPageSWR } from '../stores/page';
 
 
 
 
 declare global {
 declare global {
@@ -188,7 +189,7 @@ type Props = CommonProps & {
   sidebarConfig: ISidebarConfig,
   sidebarConfig: ISidebarConfig,
 };
 };
 
 
-const GrowiPage: NextPage<Props> = (props: Props) => {
+const Page: NextPageWithLayout<Props> = (props: Props) => {
   // const { t } = useTranslation();
   // const { t } = useTranslation();
   const router = useRouter();
   const router = useRouter();
 
 
@@ -260,24 +261,24 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
 
 
   useCurrentPageId(pageId ?? null);
   useCurrentPageId(pageId ?? null);
   useRevisionIdHackmdSynced(pageWithMeta?.data.revisionHackmdSynced);
   useRevisionIdHackmdSynced(pageWithMeta?.data.revisionHackmdSynced);
-  useRemoteRevisionId(pageWithMeta?.data.revision._id);
+  useRemoteRevisionId(pageWithMeta?.data.revision?._id);
   usePageIdOnHackmd(pageWithMeta?.data.pageIdOnHackmd);
   usePageIdOnHackmd(pageWithMeta?.data.pageIdOnHackmd);
   useHasDraftOnHackmd(pageWithMeta?.data.hasDraftOnHackmd ?? false);
   useHasDraftOnHackmd(pageWithMeta?.data.hasDraftOnHackmd ?? false);
   // useIsNotCreatable(props.isForbidden || !isCreatablePage(pagePath)); // TODO: need to include props.isIdentical
   // useIsNotCreatable(props.isForbidden || !isCreatablePage(pagePath)); // TODO: need to include props.isIdentical
   useCurrentPathname(props.currentPathname);
   useCurrentPathname(props.currentPathname);
 
 
-  const { data: currentPage } = useSWRxCurrentPage(undefined, pageWithMeta?.data ?? null); // store initial data
+  useSWRxCurrentPage(undefined, pageWithMeta?.data ?? null); // store initial data
 
 
   useEditingMarkdown(pageWithMeta?.data.revision?.body);
   useEditingMarkdown(pageWithMeta?.data.revision?.body);
 
 
   const { data: grantData } = useSWRxIsGrantNormalized(pageId);
   const { data: grantData } = useSWRxIsGrantNormalized(pageId);
   const { mutate: mutateSelectedGrant } = useSelectedGrant();
   const { mutate: mutateSelectedGrant } = useSelectedGrant();
 
 
-  const { getClassNamesByEditorMode } = useEditorMode();
-
   useSetupGlobalSocket();
   useSetupGlobalSocket();
   useSetupGlobalSocketForPage(pageId);
   useSetupGlobalSocketForPage(pageId);
 
 
+  const growiLayoutFluidClass = useCurrentGrowiLayoutFluidClassName();
+
   const shouldRenderPutbackPageModal = pageWithMeta != null
   const shouldRenderPutbackPageModal = pageWithMeta != null
     ? _isTrashPage(pageWithMeta.data.path)
     ? _isTrashPage(pageWithMeta.data.path)
     : false;
     : false;
@@ -291,93 +292,87 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
   useEffect(() => {
   useEffect(() => {
     const decodedURI = decodeURI(window.location.pathname);
     const decodedURI = decodeURI(window.location.pathname);
     if (isClient() && decodedURI !== props.currentPathname) {
     if (isClient() && decodedURI !== props.currentPathname) {
-      router.replace(props.currentPathname, undefined, { shallow: true });
+      const { search, hash } = window.location;
+      router.replace(`${props.currentPathname}${search}${hash}`, undefined, { shallow: true });
     }
     }
   }, [props.currentPathname, router]);
   }, [props.currentPathname, router]);
 
 
-  const classNames: string[] = [];
-
-  const isSidebar = pagePath === '/Sidebar';
-  classNames.push(...getClassNamesByEditorMode(isSidebar));
-
   const isTopPagePath = isTopPage(pageWithMeta?.data.path ?? '');
   const isTopPagePath = isTopPage(pageWithMeta?.data.path ?? '');
 
 
-  const isContainerFluidEachPage = currentPage == null || !('expandContentWidth' in currentPage)
-    ? null
-    : currentPage.expandContentWidth;
-  const isContainerFluidDefault = props.isContainerFluid;
-  const isContainerFluid = isContainerFluidEachPage ?? isContainerFluidDefault;
+  const title = generateCustomTitle(props, 'GROWI');
 
 
   return (
   return (
     <>
     <>
       <Head>
       <Head>
-        {/*
-        {renderScriptTagByName('drawio-viewer')}
-        {renderScriptTagByName('highlight-addons')}
-        {renderHighlightJsStyleTag(props.highlightJsStyle)}
-        */}
+        <title>{title}</title>
       </Head>
       </Head>
-
-      <DrawioViewerScript />
-
-      <BasicLayout title={useCustomTitle(props, 'GROWI')} className={classNames.join(' ')} expandContainer={isContainerFluid}>
-
-        <div className="h-100 d-flex flex-column justify-content-between">
-          <header className="py-0 position-relative">
-            <div id="grw-subnav-container">
-              <GrowiContextualSubNavigation isLinkSharingDisabled={props.disableLinkSharing} />
-            </div>
-          </header>
-          <div className="d-edit-none">
-            <GrowiSubNavigationSwitcher />
+      <div className={`dynamic-layout-root ${growiLayoutFluidClass} h-100 d-flex flex-column justify-content-between`}>
+        <header className="py-0 position-relative">
+          <div id="grw-subnav-container">
+            <GrowiContextualSubNavigation isLinkSharingDisabled={props.disableLinkSharing} />
           </div>
           </div>
+        </header>
+        <div className="d-edit-none">
+          <GrowiSubNavigationSwitcher />
+        </div>
+
+        <div id="grw-subnav-sticky-trigger" className="sticky-top"></div>
+        <div id="grw-fav-sticky-trigger" className="sticky-top"></div>
+
+        <div className="flex-grow-1">
+          <div id="main" className={`main ${isUsersHomePage(props.currentPathname) && 'user-page'}`}>
+            <div id="content-main" className="content-main grw-container-convertible">
+              { props.isIdenticalPathPage && <IdenticalPathPage /> }
+
+              { !props.isIdenticalPathPage && (
+                <>
+                  <PageAlerts />
+                  { props.isForbidden && <ForbiddenPage /> }
+                  { props.isNotCreatablePage && <NotCreatablePage />}
+                  { !props.isForbidden && !props.isNotCreatablePage && <DisplaySwitcher />}
+                  {/* <DisplaySwitcher /> */}
+                  <PageStatusAlert />
+                </>
+              ) }
 
 
-          <div id="grw-subnav-sticky-trigger" className="sticky-top"></div>
-          <div id="grw-fav-sticky-trigger" className="sticky-top"></div>
-
-          <div className="flex-grow-1">
-            <div id="main" className={`main ${isUsersHomePage(props.currentPathname) && 'user-page'}`}>
-              <div id="content-main" className="content-main grw-container-convertible">
-                { props.isIdenticalPathPage && <IdenticalPathPage /> }
-
-                { !props.isIdenticalPathPage && (
-                  <>
-                    <PageAlerts />
-                    { props.isForbidden && <ForbiddenPage /> }
-                    { props.isNotCreatablePage && <NotCreatablePage />}
-                    { !props.isForbidden && !props.isNotCreatablePage && <DisplaySwitcher />}
-                    {/* <DisplaySwitcher /> */}
-                    <PageStatusAlert />
-                  </>
-                ) }
-
-                {/* <div className="col-xl-2 col-lg-3 d-none d-lg-block revision-toc-container">
-                  <div id="revision-toc" className="revision-toc mt-3 sps sps--abv" data-sps-offset="123">
-                    <div id="revision-toc-content" className="revision-toc-content"></div>
-                  </div>
-                </div> */}
-              </div>
+              {/* <div className="col-xl-2 col-lg-3 d-none d-lg-block revision-toc-container">
+                <div id="revision-toc" className="revision-toc mt-3 sps sps--abv" data-sps-offset="123">
+                  <div id="revision-toc-content" className="revision-toc-content"></div>
+                </div>
+              </div> */}
             </div>
             </div>
           </div>
           </div>
-          { !props.isIdenticalPathPage && !props.isNotFound && (
-            <footer className="footer d-edit-none">
-              { pageWithMeta != null && pagePath != null && !isTopPagePath && (
-                <Comments pageId={pageId} pagePath={pagePath} revision={pageWithMeta.data.revision} />
-              ) }
-              { pageWithMeta != null && isUsersHomePage(pageWithMeta.data.path) && (
-                <UsersHomePageFooter creatorId={pageWithMeta.data.creator._id}/>
-              ) }
-              <CurrentPageContentFooter />
-            </footer>
-          )}
-
-          <UnsavedAlertDialog />
-          <DescendantsPageListModal />
-          <DrawioModal />
-          <HandsontableModal />
-          {shouldRenderPutbackPageModal && <PutbackPageModal />}
         </div>
         </div>
-      </BasicLayout>
+        { !props.isIdenticalPathPage && !props.isNotFound && (
+          <footer className="footer d-edit-none">
+            { pageWithMeta != null && pagePath != null && !isTopPagePath && (
+              <Comments pageId={pageId} pagePath={pagePath} revision={pageWithMeta.data.revision} />
+            ) }
+            { pageWithMeta != null && isUsersHomePage(pageWithMeta.data.path) && (
+              <UsersHomePageFooter creatorId={pageWithMeta.data.creator._id}/>
+            ) }
+            <CurrentPageContentFooter />
+          </footer>
+        )}
+
+        {shouldRenderPutbackPageModal && <PutbackPageModal />}
+      </div>
+    </>
+  );
+};
+
+Page.getLayout = function getLayout(page) {
+  return (
+    <>
+      <DrawioViewerScript />
+
+      <BasicLayoutWithEditorMode>
+        {page}
+      </BasicLayoutWithEditorMode>
+      <UnsavedAlertDialog />
+      <DescendantsPageListModal />
+      <DrawioModal />
+      <HandsontableModal />
     </>
     </>
   );
   );
 };
 };
@@ -645,4 +640,4 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
   };
   };
 };
 };
 
 
-export default GrowiPage;
+export default Page;

+ 13 - 3
packages/app/src/pages/_app.page.tsx

@@ -1,6 +1,7 @@
-import React, { useEffect } from 'react';
+import React, { ReactElement, ReactNode, useEffect } from 'react';
 
 
 import { isServer } from '@growi/core';
 import { isServer } from '@growi/core';
+import { NextPage } from 'next';
 import { appWithTranslation } from 'next-i18next';
 import { appWithTranslation } from 'next-i18next';
 import { AppProps } from 'next/app';
 import { AppProps } from 'next/app';
 import { SWRConfig } from 'swr';
 import { SWRConfig } from 'swr';
@@ -32,9 +33,15 @@ const swrConfig: SWRConfigValue = {
 };
 };
 
 
 
 
+// eslint-disable-next-line @typescript-eslint/ban-types
+export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
+  getLayout?: (page: ReactElement) => ReactNode,
+}
+
 type GrowiAppProps = AppProps & {
 type GrowiAppProps = AppProps & {
-  pageProps: CommonProps;
+  Component: NextPageWithLayout,
 };
 };
+
 // register custom serializer
 // register custom serializer
 registerTransformerForObjectId();
 registerTransformerForObjectId();
 
 
@@ -58,9 +65,12 @@ function GrowiApp({ Component, pageProps }: GrowiAppProps): JSX.Element {
   useGrowiVersion(commonPageProps.growiVersion);
   useGrowiVersion(commonPageProps.growiVersion);
   useCustomizedLogoSrc(commonPageProps.customizedLogoSrc);
   useCustomizedLogoSrc(commonPageProps.customizedLogoSrc);
 
 
+  // Use the layout defined at the page level, if available
+  const getLayout = Component.getLayout ?? (page => page);
+
   return (
   return (
     <SWRConfig value={swrConfig}>
     <SWRConfig value={swrConfig}>
-      <Component {...pageProps} />
+      {getLayout(<Component {...pageProps} />)}
     </SWRConfig>
     </SWRConfig>
   );
   );
 }
 }

+ 34 - 10
packages/app/src/pages/_document.page.tsx

@@ -73,7 +73,9 @@ const HeadersForGrowiPlugin = (props: HeadersForGrowiPluginProps): JSX.Element =
 
 
 interface GrowiDocumentProps {
 interface GrowiDocumentProps {
   theme: string,
   theme: string,
-  customCss: string;
+  customScript: string | null,
+  customCss: string | null,
+  customNoscript: string | null,
   presetThemesManifest: ViteManifest,
   presetThemesManifest: ViteManifest,
   pluginThemeHref: string | undefined,
   pluginThemeHref: string | undefined,
   pluginResourceEntries: GrowiPluginResourceEntries;
   pluginResourceEntries: GrowiPluginResourceEntries;
@@ -88,7 +90,9 @@ class GrowiDocument extends Document<GrowiDocumentInitialProps> {
     const { configManager, customizeService, pluginService } = crowi;
     const { configManager, customizeService, pluginService } = crowi;
 
 
     const theme = configManager.getConfig('crowi', 'customize:theme');
     const theme = configManager.getConfig('crowi', 'customize:theme');
-    const customCss: string = customizeService.getCustomCss();
+    const customScript: string | null = customizeService.getCustomScript();
+    const customCss: string | null = customizeService.getCustomCss();
+    const customNoscript: string | null = customizeService.getCustomNoscript();
 
 
     // import preset-themes manifest
     // import preset-themes manifest
     const presetThemesManifest = await import('@growi/preset-themes/dist/themes/manifest.json').then(imported => imported.default);
     const presetThemesManifest = await import('@growi/preset-themes/dist/themes/manifest.json').then(imported => imported.default);
@@ -100,28 +104,46 @@ class GrowiDocument extends Document<GrowiDocumentInitialProps> {
     return {
     return {
       ...initialProps,
       ...initialProps,
       theme,
       theme,
+      customScript,
       customCss,
       customCss,
+      customNoscript,
       presetThemesManifest,
       presetThemesManifest,
       pluginThemeHref,
       pluginThemeHref,
       pluginResourceEntries,
       pluginResourceEntries,
     };
     };
   }
   }
 
 
+  renderCustomScript(customScript: string | null): JSX.Element {
+    if (customScript == null || customScript.length === 0) {
+      return <></>;
+    }
+    return <script id="customScript" dangerouslySetInnerHTML={{ __html: customScript }} />;
+  }
+
+  renderCustomCss(customCss: string | null): JSX.Element {
+    if (customCss == null || customCss.length === 0) {
+      return <></>;
+    }
+    return <style dangerouslySetInnerHTML={{ __html: customCss }} />;
+  }
+
+  renderCustomNoscript(customNoscript: string | null): JSX.Element {
+    if (customNoscript == null || customNoscript.length === 0) {
+      return <></>;
+    }
+    return <noscript dangerouslySetInnerHTML={{ __html: customNoscript }} />;
+  }
+
   override render(): JSX.Element {
   override render(): JSX.Element {
     const {
     const {
-      customCss, theme, presetThemesManifest, pluginThemeHref, pluginResourceEntries,
+      customCss, customScript, customNoscript,
+      theme, presetThemesManifest, pluginThemeHref, pluginResourceEntries,
     } = this.props;
     } = this.props;
 
 
     return (
     return (
       <Html>
       <Html>
         <Head>
         <Head>
-          <style>
-            {customCss}
-          </style>
-          {/*
-          {renderScriptTagsByGroup('basis')}
-          {renderStyleTagsByGroup('basis')}
-          */}
+          {this.renderCustomScript(customScript)}
           <link rel='preload' href="/static/fonts/PressStart2P-latin.woff2" as="font" type="font/woff2" />
           <link rel='preload' href="/static/fonts/PressStart2P-latin.woff2" as="font" type="font/woff2" />
           <link rel='preload' href="/static/fonts/PressStart2P-latin-ext.woff2" as="font" type="font/woff2" />
           <link rel='preload' href="/static/fonts/PressStart2P-latin-ext.woff2" as="font" type="font/woff2" />
           <link rel='preload' href="/static/fonts/Lato-Regular-latin.woff2" as="font" type="font/woff2" />
           <link rel='preload' href="/static/fonts/Lato-Regular-latin.woff2" as="font" type="font/woff2" />
@@ -131,8 +153,10 @@ class GrowiDocument extends Document<GrowiDocumentInitialProps> {
           <HeadersForThemes theme={theme}
           <HeadersForThemes theme={theme}
             presetThemesManifest={presetThemesManifest} pluginThemeHref={pluginThemeHref} />
             presetThemesManifest={presetThemesManifest} pluginThemeHref={pluginThemeHref} />
           <HeadersForGrowiPlugin pluginResourceEntries={pluginResourceEntries} />
           <HeadersForGrowiPlugin pluginResourceEntries={pluginResourceEntries} />
+          {this.renderCustomCss(customCss)}
         </Head>
         </Head>
         <body>
         <body>
+          {this.renderCustomNoscript(customNoscript)}
           <Main />
           <Main />
           <NextScript />
           <NextScript />
         </body>
         </body>

+ 8 - 6
packages/app/src/pages/_private-legacy-pages.page.tsx

@@ -1,6 +1,7 @@
 import {
 import {
   NextPage, GetServerSideProps, GetServerSidePropsContext,
   NextPage, GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
 } from 'next';
+import { useTranslation } from 'next-i18next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
 import Head from 'next/head';
@@ -22,7 +23,7 @@ import {
 } from '~/stores/ui';
 } from '~/stores/ui';
 
 
 import {
 import {
-  CommonProps, getNextI18NextConfig, getServerSideCommonProps, useCustomTitle,
+  CommonProps, getNextI18NextConfig, getServerSideCommonProps, generateCustomTitle,
 } from './utils/commons';
 } from './utils/commons';
 
 
 const SearchResultLayout = dynamic(() => import('~/components/Layout/SearchResultLayout'), { ssr: false });
 const SearchResultLayout = dynamic(() => import('~/components/Layout/SearchResultLayout'), { ssr: false });
@@ -47,6 +48,8 @@ type Props = CommonProps & {
 };
 };
 
 
 const PrivateLegacyPage: NextPage<Props> = (props: Props) => {
 const PrivateLegacyPage: NextPage<Props> = (props: Props) => {
+  const { t } = useTranslation();
+
   const { userUISettings } = props;
   const { userUISettings } = props;
 
 
   const PrivateLegacyPages = dynamic(() => import('~/components/PrivateLegacyPages'), { ssr: false });
   const PrivateLegacyPages = dynamic(() => import('~/components/PrivateLegacyPages'), { ssr: false });
@@ -74,18 +77,17 @@ const PrivateLegacyPage: NextPage<Props> = (props: Props) => {
   // render config
   // render config
   useRendererConfig(props.rendererConfig);
   useRendererConfig(props.rendererConfig);
 
 
+  const title = generateCustomTitle(props, t('private_legacy_pages.title'));
+
   return (
   return (
     <>
     <>
       <Head>
       <Head>
-        {/*
-        {renderScriptTagByName('drawio-viewer')}
-        {renderScriptTagByName('highlight-addons')}
-        */}
+        <title>{title}</title>
       </Head>
       </Head>
 
 
       <DrawioViewerScript />
       <DrawioViewerScript />
 
 
-      <SearchResultLayout title={useCustomTitle(props, 'GROWI')}>
+      <SearchResultLayout>
         <div id="private-regacy-pages">
         <div id="private-regacy-pages">
           <PrivateLegacyPages />
           <PrivateLegacyPages />
         </div>
         </div>

+ 17 - 17
packages/app/src/pages/_search.page.tsx

@@ -24,8 +24,9 @@ import {
 import { SearchPage } from '../components/SearchPage';
 import { SearchPage } from '../components/SearchPage';
 
 
 import {
 import {
-  CommonProps, getNextI18NextConfig, getServerSideCommonProps, useCustomTitle,
+  CommonProps, getNextI18NextConfig, getServerSideCommonProps, generateCustomTitle,
 } from './utils/commons';
 } from './utils/commons';
+import { NextPageWithLayout } from './_app.page';
 
 
 const SearchResultLayout = dynamic(() => import('~/components/Layout/SearchResultLayout'), { ssr: false });
 const SearchResultLayout = dynamic(() => import('~/components/Layout/SearchResultLayout'), { ssr: false });
 
 
@@ -53,7 +54,7 @@ type Props = CommonProps & {
 
 
 };
 };
 
 
-const SearchResultPage: NextPage<Props> = (props: Props) => {
+const SearchResultPage: NextPageWithLayout<Props> = (props: Props) => {
   const { userUISettings } = props;
   const { userUISettings } = props;
 
 
   // commons
   // commons
@@ -87,33 +88,32 @@ const SearchResultPage: NextPage<Props> = (props: Props) => {
     return <PutbackPageModal />;
     return <PutbackPageModal />;
   };
   };
 
 
-  const classNames: string[] = [];
-  // if (props.isContainerFluid) {
-  //   classNames.push('growi-layout-fluid');
-  // }
+  const title = generateCustomTitle(props, 'GROWI');
 
 
   return (
   return (
     <>
     <>
       <Head>
       <Head>
-        {/*
-        {renderScriptTagByName('drawio-viewer')}
-        {renderScriptTagByName('highlight-addons')}
-        */}
+        <title>{title}</title>
       </Head>
       </Head>
 
 
-      <DrawioViewerScript />
-
-      <SearchResultLayout title={useCustomTitle(props, 'GROWI')} className={classNames.join(' ')}>
-        <div id="search-page">
-          <SearchPage />
-        </div>
-      </SearchResultLayout>
+      <div id="search-page" className="dynamic-layout-root">
+        <SearchPage />
+      </div>
 
 
       <PutbackPageModal />
       <PutbackPageModal />
     </>
     </>
   );
   );
 };
 };
 
 
+SearchResultPage.getLayout = function getLayout(page) {
+  return (
+    <>
+      <DrawioViewerScript />
+      <SearchResultLayout>{page}</SearchResultLayout>
+    </>
+  );
+};
+
 async function injectUserUISettings(context: GetServerSidePropsContext, props: Props): Promise<void> {
 async function injectUserUISettings(context: GetServerSidePropsContext, props: Props): Promise<void> {
   const { model: mongooseModel } = await import('mongoose');
   const { model: mongooseModel } = await import('mongoose');
 
 

+ 7 - 1
packages/app/src/pages/admin/[...path].page.tsx

@@ -2,8 +2,9 @@ import {
   NextPage, GetServerSideProps, GetServerSidePropsContext,
   NextPage, GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
 } from 'next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
+import Head from 'next/head';
 
 
-import { CommonProps } from '~/pages/utils/commons';
+import { CommonProps, generateCustomTitle } from '~/pages/utils/commons';
 import { useCurrentUser } from '~/stores/context';
 import { useCurrentUser } from '~/stores/context';
 import { useIsMaintenanceMode } from '~/stores/maintenanceMode';
 import { useIsMaintenanceMode } from '~/stores/maintenanceMode';
 
 
@@ -17,8 +18,13 @@ const AdminAppPage: NextPage<CommonProps> = (props) => {
   useIsMaintenanceMode(props.isMaintenanceMode);
   useIsMaintenanceMode(props.isMaintenanceMode);
   useCurrentUser(props.currentUser ?? null);
   useCurrentUser(props.currentUser ?? null);
 
 
+  const title = generateCustomTitle(props, 'GROWI');
+
   return (
   return (
     <AdminLayout>
     <AdminLayout>
+      <Head>
+        <title>{title}</title>
+      </Head>
       <AdminNotFoundPage />
       <AdminNotFoundPage />
     </AdminLayout>
     </AdminLayout>
   );
   );

+ 8 - 3
packages/app/src/pages/admin/app.page.tsx

@@ -4,11 +4,12 @@ import {
 } from 'next';
 } from 'next';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
+import Head from 'next/head';
 import { Container, Provider } from 'unstated';
 import { Container, Provider } from 'unstated';
 
 
 
 
 import AdminAppContainer from '~/client/services/AdminAppContainer';
 import AdminAppContainer from '~/client/services/AdminAppContainer';
-import { CommonProps, useCustomTitle } from '~/pages/utils/commons';
+import { CommonProps, generateCustomTitle } from '~/pages/utils/commons';
 import { useCurrentUser } from '~/stores/context';
 import { useCurrentUser } from '~/stores/context';
 import { useIsMaintenanceMode } from '~/stores/maintenanceMode';
 import { useIsMaintenanceMode } from '~/stores/maintenanceMode';
 
 
@@ -24,7 +25,6 @@ const AdminAppPage: NextPage<CommonProps> = (props) => {
   useIsMaintenanceMode(props.isMaintenanceMode);
   useIsMaintenanceMode(props.isMaintenanceMode);
   useCurrentUser(props.currentUser ?? null);
   useCurrentUser(props.currentUser ?? null);
 
 
-  const title = t('headers.app_settings');
   const injectableContainers: Container<any>[] = [];
   const injectableContainers: Container<any>[] = [];
 
 
   if (isClient()) {
   if (isClient()) {
@@ -32,9 +32,14 @@ const AdminAppPage: NextPage<CommonProps> = (props) => {
     injectableContainers.push(adminAppContainer);
     injectableContainers.push(adminAppContainer);
   }
   }
 
 
+  const title = generateCustomTitle(props, t('headers.app_settings'));
+
   return (
   return (
     <Provider inject={[...injectableContainers]}>
     <Provider inject={[...injectableContainers]}>
-      <AdminLayout title={useCustomTitle(props, title)} componentTitle={title} >
+      <AdminLayout componentTitle={title}>
+        <Head>
+          <title>{title}</title>
+        </Head>
         <AppSettingsPageContents />
         <AppSettingsPageContents />
       </AdminLayout>
       </AdminLayout>
     </Provider>
     </Provider>

+ 7 - 2
packages/app/src/pages/admin/audit-log.page.tsx

@@ -3,10 +3,11 @@ import {
 } from 'next';
 } from 'next';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
+import Head from 'next/head';
 
 
 import { SupportedActionType } from '~/interfaces/activity';
 import { SupportedActionType } from '~/interfaces/activity';
 import { CrowiRequest } from '~/interfaces/crowi-request';
 import { CrowiRequest } from '~/interfaces/crowi-request';
-import { CommonProps, useCustomTitle } from '~/pages/utils/commons';
+import { CommonProps, generateCustomTitle } from '~/pages/utils/commons';
 import { useCurrentUser, useAuditLogEnabled, useAuditLogAvailableActions } from '~/stores/context';
 import { useCurrentUser, useAuditLogEnabled, useAuditLogAvailableActions } from '~/stores/context';
 
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
@@ -29,9 +30,13 @@ const AdminAuditLogPage: NextPage<Props> = (props) => {
   useCurrentUser(props.currentUser ?? null);
   useCurrentUser(props.currentUser ?? null);
 
 
   const title = t('audit_log_management.audit_log');
   const title = t('audit_log_management.audit_log');
+  const headTitle = generateCustomTitle(props, title);
 
 
   return (
   return (
-    <AdminLayout title={useCustomTitle(props, title)} componentTitle={title} >
+    <AdminLayout componentTitle={title}>
+      <Head>
+        <title>{headTitle}</title>
+      </Head>
       <AuditLogManagement />
       <AuditLogManagement />
     </AdminLayout>
     </AdminLayout>
   );
   );

+ 8 - 3
packages/app/src/pages/admin/customize.page.tsx

@@ -4,11 +4,12 @@ import {
 } from 'next';
 } from 'next';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
+import Head from 'next/head';
 import { Container, Provider } from 'unstated';
 import { Container, Provider } from 'unstated';
 
 
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
 import { CrowiRequest } from '~/interfaces/crowi-request';
 import { CrowiRequest } from '~/interfaces/crowi-request';
-import { CommonProps, useCustomTitle } from '~/pages/utils/commons';
+import { CommonProps, generateCustomTitle } from '~/pages/utils/commons';
 import { useCustomizeTitle, useCurrentUser } from '~/stores/context';
 import { useCustomizeTitle, useCurrentUser } from '~/stores/context';
 
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
@@ -27,7 +28,8 @@ const AdminCustomizeSettingsPage: NextPage<Props> = (props) => {
   useCustomizeTitle(props.customizeTitle);
   useCustomizeTitle(props.customizeTitle);
   useCurrentUser(props.currentUser ?? null);
   useCurrentUser(props.currentUser ?? null);
 
 
-  const title = t('customize_settings.customize_settings');
+  const componentTitle = t('customize_settings.customize_settings');
+  const pageTitle = generateCustomTitle(props, componentTitle);
   const injectableContainers: Container<any>[] = [];
   const injectableContainers: Container<any>[] = [];
 
 
   if (isClient()) {
   if (isClient()) {
@@ -39,7 +41,10 @@ const AdminCustomizeSettingsPage: NextPage<Props> = (props) => {
 
 
   return (
   return (
     <Provider inject={[...injectableContainers]}>
     <Provider inject={[...injectableContainers]}>
-      <AdminLayout title={useCustomTitle(props, title)} componentTitle={title} >
+      <AdminLayout componentTitle={componentTitle}>
+        <Head>
+          <title>{pageTitle}</title>
+        </Head>
         <CustomizeSettingContents />
         <CustomizeSettingContents />
       </AdminLayout>
       </AdminLayout>
     </Provider>
     </Provider>

+ 8 - 3
packages/app/src/pages/admin/export.page.tsx

@@ -4,10 +4,11 @@ import {
 } from 'next';
 } from 'next';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
+import Head from 'next/head';
 import { Container, Provider } from 'unstated';
 import { Container, Provider } from 'unstated';
 
 
 import AdminAppContainer from '~/client/services/AdminAppContainer';
 import AdminAppContainer from '~/client/services/AdminAppContainer';
-import { CommonProps, useCustomTitle } from '~/pages/utils/commons';
+import { CommonProps, generateCustomTitle } from '~/pages/utils/commons';
 import { useCurrentUser } from '~/stores/context';
 import { useCurrentUser } from '~/stores/context';
 
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
@@ -20,7 +21,8 @@ const AdminExportDataArchivePage: NextPage<CommonProps> = (props) => {
   const { t } = useTranslation('admin');
   const { t } = useTranslation('admin');
   useCurrentUser(props.currentUser ?? null);
   useCurrentUser(props.currentUser ?? null);
 
 
-  const title = t('export_management.export_archive_data');
+  const componentTitle = t('export_management.export_archive_data');
+  const pageTitle = generateCustomTitle(props, componentTitle);
   const injectableContainers: Container<any>[] = [];
   const injectableContainers: Container<any>[] = [];
 
 
   if (isClient()) {
   if (isClient()) {
@@ -30,7 +32,10 @@ const AdminExportDataArchivePage: NextPage<CommonProps> = (props) => {
 
 
   return (
   return (
     <Provider inject={[...injectableContainers]}>
     <Provider inject={[...injectableContainers]}>
-      <AdminLayout title={useCustomTitle(props, title)} componentTitle={title} >
+      <AdminLayout componentTitle={componentTitle}>
+        <Head>
+          <title>{pageTitle}</title>
+        </Head>
         <ExportArchiveDataPage />
         <ExportArchiveDataPage />
       </AdminLayout>
       </AdminLayout>
     </Provider>
     </Provider>

+ 7 - 3
packages/app/src/pages/admin/global-notification/[globalNotificationId].page.tsx

@@ -6,13 +6,14 @@ import {
 } from 'next';
 } from 'next';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
+import Head from 'next/head';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
 import { Container, Provider } from 'unstated';
 import { Container, Provider } from 'unstated';
 
 
 
 
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
 import { toastError } from '~/client/util/apiNotification';
 import { toastError } from '~/client/util/apiNotification';
-import { CommonProps, useCustomTitle } from '~/pages/utils/commons';
+import { CommonProps, generateCustomTitle } from '~/pages/utils/commons';
 import { useCurrentUser } from '~/stores/context';
 import { useCurrentUser } from '~/stores/context';
 
 
 import { retrieveServerSideProps } from '../../../utils/admin-page-util';
 import { retrieveServerSideProps } from '../../../utils/admin-page-util';
@@ -43,7 +44,7 @@ const AdminGlobalNotificationNewPage: NextPage<CommonProps> = (props) => {
 
 
 
 
   const title = t('external_notification.external_notification');
   const title = t('external_notification.external_notification');
-  const customTitle = useCustomTitle(props, title);
+  const customTitle = generateCustomTitle(props, title);
 
 
 
 
   const injectableContainers: Container<any>[] = [];
   const injectableContainers: Container<any>[] = [];
@@ -56,7 +57,10 @@ const AdminGlobalNotificationNewPage: NextPage<CommonProps> = (props) => {
 
 
   return (
   return (
     <Provider inject={[...injectableContainers]}>
     <Provider inject={[...injectableContainers]}>
-      <AdminLayout title={customTitle} componentTitle={title} >
+      <AdminLayout componentTitle={title}>
+        <Head>
+          <title>{customTitle}</title>
+        </Head>
         {
         {
           currentGlobalNotificationId != null && router.isReady
           currentGlobalNotificationId != null && router.isReady
       && <ManageGlobalNotification globalNotificationId={currentGlobalNotificationId} />
       && <ManageGlobalNotification globalNotificationId={currentGlobalNotificationId} />

+ 6 - 2
packages/app/src/pages/admin/global-notification/new.page.tsx

@@ -4,10 +4,11 @@ import {
 } from 'next';
 } from 'next';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
+import Head from 'next/head';
 import { Container, Provider } from 'unstated';
 import { Container, Provider } from 'unstated';
 
 
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
-import { CommonProps, useCustomTitle } from '~/pages/utils/commons';
+import { CommonProps, generateCustomTitle } from '~/pages/utils/commons';
 import { useCurrentUser } from '~/stores/context';
 import { useCurrentUser } from '~/stores/context';
 
 
 import { retrieveServerSideProps } from '../../../utils/admin-page-util';
 import { retrieveServerSideProps } from '../../../utils/admin-page-util';
@@ -31,7 +32,10 @@ const AdminGlobalNotificationNewPage: NextPage<CommonProps> = (props) => {
 
 
   return (
   return (
     <Provider inject={[...injectableContainers]}>
     <Provider inject={[...injectableContainers]}>
-      <AdminLayout title={useCustomTitle(props, title)} componentTitle={title} >
+      <AdminLayout componentTitle={title}>
+        <Head>
+          <title>{title}</title>
+        </Head>
         <ManageGlobalNotification />
         <ManageGlobalNotification />
       </AdminLayout>
       </AdminLayout>
     </Provider>
     </Provider>

+ 8 - 3
packages/app/src/pages/admin/importer.page.tsx

@@ -4,10 +4,11 @@ import {
 } from 'next';
 } from 'next';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
+import Head from 'next/head';
 import { Container, Provider } from 'unstated';
 import { Container, Provider } from 'unstated';
 
 
 import AdminImportContainer from '~/client/services/AdminImportContainer';
 import AdminImportContainer from '~/client/services/AdminImportContainer';
-import { CommonProps, useCustomTitle } from '~/pages/utils/commons';
+import { CommonProps, generateCustomTitle } from '~/pages/utils/commons';
 import { useCurrentUser } from '~/stores/context';
 import { useCurrentUser } from '~/stores/context';
 
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
@@ -20,7 +21,8 @@ const AdminDataImportPage: NextPage<CommonProps> = (props) => {
   const { t } = useTranslation('admin');
   const { t } = useTranslation('admin');
   useCurrentUser(props.currentUser ?? null);
   useCurrentUser(props.currentUser ?? null);
 
 
-  const title = t('importer_management.import_data');
+  const componentTitle = t('importer_management.import_data');
+  const pageTitle = generateCustomTitle(props, componentTitle);
   const injectableContainers: Container<any>[] = [];
   const injectableContainers: Container<any>[] = [];
 
 
   if (isClient()) {
   if (isClient()) {
@@ -31,7 +33,10 @@ const AdminDataImportPage: NextPage<CommonProps> = (props) => {
 
 
   return (
   return (
     <Provider inject={[...injectableContainers]}>
     <Provider inject={[...injectableContainers]}>
-      <AdminLayout title={useCustomTitle(props, title)} componentTitle={title} >
+      <AdminLayout componentTitle={componentTitle}>
+        <Head>
+          <title>{pageTitle}</title>
+        </Head>
         <DataImportPageContents />
         <DataImportPageContents />
       </AdminLayout>
       </AdminLayout>
     </Provider>
     </Provider>

+ 6 - 2
packages/app/src/pages/admin/index.page.tsx

@@ -4,11 +4,12 @@ import {
 } from 'next';
 } from 'next';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
+import Head from 'next/head';
 import { Container, Provider } from 'unstated';
 import { Container, Provider } from 'unstated';
 
 
 import AdminHomeContainer from '~/client/services/AdminHomeContainer';
 import AdminHomeContainer from '~/client/services/AdminHomeContainer';
 import { CrowiRequest } from '~/interfaces/crowi-request';
 import { CrowiRequest } from '~/interfaces/crowi-request';
-import { CommonProps, useCustomTitle } from '~/pages/utils/commons';
+import { CommonProps, generateCustomTitle } from '~/pages/utils/commons';
 import { useCurrentUser, useGrowiCloudUri, useGrowiAppIdForGrowiCloud } from '~/stores/context';
 import { useCurrentUser, useGrowiCloudUri, useGrowiAppIdForGrowiCloud } from '~/stores/context';
 
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
@@ -46,7 +47,10 @@ const AdminHomePage: NextPage<Props> = (props) => {
 
 
   return (
   return (
     <Provider inject={[...injectableContainers]}>
     <Provider inject={[...injectableContainers]}>
-      <AdminLayout title={useCustomTitle(props, title)} componentTitle={title} >
+      <AdminLayout componentTitle={title}>
+        <Head>
+          <title>{title}</title>
+        </Head>
         <AdminHome
         <AdminHome
           nodeVersion={props.nodeVersion}
           nodeVersion={props.nodeVersion}
           npmVersion={props.npmVersion}
           npmVersion={props.npmVersion}

+ 9 - 3
packages/app/src/pages/admin/markdown.page.tsx

@@ -4,11 +4,12 @@ import {
 } from 'next';
 } from 'next';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
+import Head from 'next/head';
 import { Container, Provider } from 'unstated';
 import { Container, Provider } from 'unstated';
 
 
 
 
 import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
 import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
-import { CommonProps, useCustomTitle } from '~/pages/utils/commons';
+import { CommonProps, generateCustomTitle } from '~/pages/utils/commons';
 import { useCurrentUser } from '~/stores/context';
 import { useCurrentUser } from '~/stores/context';
 
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
@@ -21,7 +22,9 @@ const AdminMarkdownPage: NextPage<CommonProps> = (props) => {
   const { t } = useTranslation('admin');
   const { t } = useTranslation('admin');
   useCurrentUser(props.currentUser ?? null);
   useCurrentUser(props.currentUser ?? null);
 
 
-  const title = t('markdown_settings.markdown_settings');
+  const componentTitle = t('markdown_settings.markdown_settings');
+  const pageTitle = generateCustomTitle(props, componentTitle);
+
   const injectableContainers: Container<any>[] = [];
   const injectableContainers: Container<any>[] = [];
 
 
   if (isClient()) {
   if (isClient()) {
@@ -32,7 +35,10 @@ const AdminMarkdownPage: NextPage<CommonProps> = (props) => {
 
 
   return (
   return (
     <Provider inject={[...injectableContainers]}>
     <Provider inject={[...injectableContainers]}>
-      <AdminLayout title={useCustomTitle(props, title)} componentTitle={title} >
+      <AdminLayout componentTitle={componentTitle}>
+        <Head>
+          <title>{pageTitle}</title>
+        </Head>
         <MarkDownSettingContents />
         <MarkDownSettingContents />
       </AdminLayout>
       </AdminLayout>
     </Provider>
     </Provider>

+ 8 - 3
packages/app/src/pages/admin/notification.page.tsx

@@ -4,10 +4,11 @@ import {
 } from 'next';
 } from 'next';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
+import Head from 'next/head';
 import { Container, Provider } from 'unstated';
 import { Container, Provider } from 'unstated';
 
 
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
 import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
-import { CommonProps, useCustomTitle } from '~/pages/utils/commons';
+import { CommonProps, generateCustomTitle } from '~/pages/utils/commons';
 import { useCurrentUser } from '~/stores/context';
 import { useCurrentUser } from '~/stores/context';
 
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
@@ -20,7 +21,8 @@ const AdminExternalNotificationPage: NextPage<CommonProps> = (props) => {
   const { t } = useTranslation('admin');
   const { t } = useTranslation('admin');
   useCurrentUser(props.currentUser ?? null);
   useCurrentUser(props.currentUser ?? null);
 
 
-  const title = t('external_notification.external_notification');
+  const componentTitle = t('external_notification.external_notification');
+  const pageTitle = generateCustomTitle(props, componentTitle);
   const injectableContainers: Container<any>[] = [];
   const injectableContainers: Container<any>[] = [];
 
 
   if (isClient()) {
   if (isClient()) {
@@ -32,7 +34,10 @@ const AdminExternalNotificationPage: NextPage<CommonProps> = (props) => {
 
 
   return (
   return (
     <Provider inject={[...injectableContainers]}>
     <Provider inject={[...injectableContainers]}>
-      <AdminLayout title={useCustomTitle(props, title)} componentTitle={title} >
+      <AdminLayout componentTitle={componentTitle}>
+        <Head>
+          <title>{pageTitle}</title>
+        </Head>
         <NotificationSetting />
         <NotificationSetting />
       </AdminLayout>
       </AdminLayout>
     </Provider>
     </Provider>

+ 6 - 2
packages/app/src/pages/admin/plugins.page.tsx

@@ -4,11 +4,12 @@ import {
 } from 'next';
 } from 'next';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
+import Head from 'next/head';
 import { Container, Provider } from 'unstated';
 import { Container, Provider } from 'unstated';
 
 
 
 
 import AdminAppContainer from '~/client/services/AdminAppContainer';
 import AdminAppContainer from '~/client/services/AdminAppContainer';
-import { CommonProps, useCustomTitle } from '~/pages/utils/commons';
+import { CommonProps, generateCustomTitle } from '~/pages/utils/commons';
 import { useCurrentUser } from '~/stores/context';
 import { useCurrentUser } from '~/stores/context';
 import { useIsMaintenanceMode } from '~/stores/maintenanceMode';
 import { useIsMaintenanceMode } from '~/stores/maintenanceMode';
 
 
@@ -37,7 +38,10 @@ const AdminAppPage: NextPage<CommonProps> = (props) => {
 
 
   return (
   return (
     <Provider inject={[...injectableContainers]}>
     <Provider inject={[...injectableContainers]}>
-      <AdminLayout title={useCustomTitle(props, title)} componentTitle={title} >
+      <AdminLayout componentTitle={title} >
+        <Head>
+          <title>{generateCustomTitle(props, title)}</title>
+        </Head>
         <PluginsExtensionPageContents />
         <PluginsExtensionPageContents />
       </AdminLayout>
       </AdminLayout>
     </Provider>
     </Provider>

+ 7 - 2
packages/app/src/pages/admin/search.page.tsx

@@ -3,9 +3,10 @@ import {
 } from 'next';
 } from 'next';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
+import Head from 'next/head';
 
 
 import { CrowiRequest } from '~/interfaces/crowi-request';
 import { CrowiRequest } from '~/interfaces/crowi-request';
-import { CommonProps, useCustomTitle } from '~/pages/utils/commons';
+import { CommonProps, generateCustomTitle } from '~/pages/utils/commons';
 import { useIsSearchServiceReachable, useCurrentUser } from '~/stores/context';
 import { useIsSearchServiceReachable, useCurrentUser } from '~/stores/context';
 
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
@@ -27,9 +28,13 @@ const AdminFullTextSearchManagementPage: NextPage<Props> = (props) => {
   useIsSearchServiceReachable(props.isSearchServiceReachable);
   useIsSearchServiceReachable(props.isSearchServiceReachable);
 
 
   const title = t('full_text_search_management.full_text_search_management');
   const title = t('full_text_search_management.full_text_search_management');
+  const headTitle = generateCustomTitle(props, title);
 
 
   return (
   return (
-    <AdminLayout title={useCustomTitle(props, title)} componentTitle={title} >
+    <AdminLayout componentTitle={title}>
+      <Head>
+        <title>{headTitle}</title>
+      </Head>
       <FullTextSearchManagement />
       <FullTextSearchManagement />
     </AdminLayout>
     </AdminLayout>
   );
   );

+ 8 - 11
packages/app/src/pages/admin/security.page.tsx

@@ -4,10 +4,9 @@ import {
 } from 'next';
 } from 'next';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
+import Head from 'next/head';
 import { Container, Provider } from 'unstated';
 import { Container, Provider } from 'unstated';
 
 
-
-import AdminBasicSecurityContainer from '~/client/services/AdminBasicSecurityContainer';
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
 import AdminGitHubSecurityContainer from '~/client/services/AdminGitHubSecurityContainer';
 import AdminGitHubSecurityContainer from '~/client/services/AdminGitHubSecurityContainer';
 import AdminGoogleSecurityContainer from '~/client/services/AdminGoogleSecurityContainer';
 import AdminGoogleSecurityContainer from '~/client/services/AdminGoogleSecurityContainer';
@@ -15,9 +14,8 @@ import AdminLdapSecurityContainer from '~/client/services/AdminLdapSecurityConta
 import AdminLocalSecurityContainer from '~/client/services/AdminLocalSecurityContainer';
 import AdminLocalSecurityContainer from '~/client/services/AdminLocalSecurityContainer';
 import AdminOidcSecurityContainer from '~/client/services/AdminOidcSecurityContainer';
 import AdminOidcSecurityContainer from '~/client/services/AdminOidcSecurityContainer';
 import AdminSamlSecurityContainer from '~/client/services/AdminSamlSecurityContainer';
 import AdminSamlSecurityContainer from '~/client/services/AdminSamlSecurityContainer';
-import AdminTwitterSecurityContainer from '~/client/services/AdminTwitterSecurityContainer';
 import { CrowiRequest } from '~/interfaces/crowi-request';
 import { CrowiRequest } from '~/interfaces/crowi-request';
-import { CommonProps, useCustomTitle } from '~/pages/utils/commons';
+import { CommonProps, generateCustomTitle } from '~/pages/utils/commons';
 import { useCurrentUser, useIsMailerSetup, useSiteUrl } from '~/stores/context';
 import { useCurrentUser, useIsMailerSetup, useSiteUrl } from '~/stores/context';
 
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
@@ -38,7 +36,8 @@ const AdminSecuritySettingsPage: NextPage<Props> = (props) => {
   useSiteUrl(props.siteUrl);
   useSiteUrl(props.siteUrl);
   useIsMailerSetup(props.isMailerSetup);
   useIsMailerSetup(props.isMailerSetup);
 
 
-  const title = t('security_settings.security_settings');
+  const componentTitle = t('security_settings.security_settings');
+  const pageTitle = generateCustomTitle(props, componentTitle);
   const adminSecurityContainers: Container<any>[] = [];
   const adminSecurityContainers: Container<any>[] = [];
 
 
   if (isClient()) {
   if (isClient()) {
@@ -50,10 +49,8 @@ const AdminSecuritySettingsPage: NextPage<Props> = (props) => {
       const adminLdapSecurityContainer = new AdminLdapSecurityContainer();
       const adminLdapSecurityContainer = new AdminLdapSecurityContainer();
       const adminSamlSecurityContainer = new AdminSamlSecurityContainer();
       const adminSamlSecurityContainer = new AdminSamlSecurityContainer();
       const adminOidcSecurityContainer = new AdminOidcSecurityContainer();
       const adminOidcSecurityContainer = new AdminOidcSecurityContainer();
-      const adminBasicSecurityContainer = new AdminBasicSecurityContainer();
       const adminGoogleSecurityContainer = new AdminGoogleSecurityContainer();
       const adminGoogleSecurityContainer = new AdminGoogleSecurityContainer();
       const adminGitHubSecurityContainer = new AdminGitHubSecurityContainer();
       const adminGitHubSecurityContainer = new AdminGitHubSecurityContainer();
-      const adminTwitterSecurityContainer = new AdminTwitterSecurityContainer();
 
 
       adminSecurityContainers.push(
       adminSecurityContainers.push(
         adminGeneralSecurityContainer,
         adminGeneralSecurityContainer,
@@ -61,18 +58,18 @@ const AdminSecuritySettingsPage: NextPage<Props> = (props) => {
         adminLdapSecurityContainer,
         adminLdapSecurityContainer,
         adminSamlSecurityContainer,
         adminSamlSecurityContainer,
         adminOidcSecurityContainer,
         adminOidcSecurityContainer,
-        adminBasicSecurityContainer,
         adminGoogleSecurityContainer,
         adminGoogleSecurityContainer,
         adminGitHubSecurityContainer,
         adminGitHubSecurityContainer,
-        adminTwitterSecurityContainer,
       );
       );
     }
     }
   }
   }
 
 
-
   return (
   return (
     <Provider inject={[...adminSecurityContainers]}>
     <Provider inject={[...adminSecurityContainers]}>
-      <AdminLayout title={useCustomTitle(props, title)} componentTitle={title} >
+      <AdminLayout componentTitle={componentTitle}>
+        <Head>
+          <title>{pageTitle}</title>
+        </Head>
         <SecurityManagement />
         <SecurityManagement />
       </AdminLayout>
       </AdminLayout>
     </Provider>
     </Provider>

+ 7 - 2
packages/app/src/pages/admin/slack-integration-legacy.page.tsx

@@ -4,10 +4,11 @@ import {
 } from 'next';
 } from 'next';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
+import Head from 'next/head';
 import { Container, Provider } from 'unstated';
 import { Container, Provider } from 'unstated';
 
 
 import AdminSlackIntegrationLegacyContainer from '~/client/services/AdminSlackIntegrationLegacyContainer';
 import AdminSlackIntegrationLegacyContainer from '~/client/services/AdminSlackIntegrationLegacyContainer';
-import { CommonProps, useCustomTitle } from '~/pages/utils/commons';
+import { CommonProps, generateCustomTitle } from '~/pages/utils/commons';
 import { useCurrentUser } from '~/stores/context';
 import { useCurrentUser } from '~/stores/context';
 
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
@@ -21,6 +22,7 @@ const AdminLegacySlackIntegrationPage: NextPage<CommonProps> = (props) => {
   useCurrentUser(props.currentUser ?? null);
   useCurrentUser(props.currentUser ?? null);
 
 
   const title = t('slack_integration_legacy.slack_integration_legacy');
   const title = t('slack_integration_legacy.slack_integration_legacy');
+  const headTitle = generateCustomTitle(props, title);
   const injectableContainers: Container<any>[] = [];
   const injectableContainers: Container<any>[] = [];
 
 
   if (isClient()) {
   if (isClient()) {
@@ -31,7 +33,10 @@ const AdminLegacySlackIntegrationPage: NextPage<CommonProps> = (props) => {
 
 
   return (
   return (
     <Provider inject={[...injectableContainers]}>
     <Provider inject={[...injectableContainers]}>
-      <AdminLayout title={useCustomTitle(props, title)} componentTitle={title} >
+      <AdminLayout componentTitle={title}>
+        <Head>
+          <title>{headTitle}</title>
+        </Head>
         <LegacySlackIntegration />
         <LegacySlackIntegration />
       </AdminLayout>
       </AdminLayout>
     </Provider>
     </Provider>

+ 8 - 3
packages/app/src/pages/admin/slack-integration.page.tsx

@@ -3,9 +3,10 @@ import {
 } from 'next';
 } from 'next';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
+import Head from 'next/head';
 
 
 import { CrowiRequest } from '~/interfaces/crowi-request';
 import { CrowiRequest } from '~/interfaces/crowi-request';
-import { CommonProps, useCustomTitle } from '~/pages/utils/commons';
+import { CommonProps, generateCustomTitle } from '~/pages/utils/commons';
 import { useCurrentUser, useSiteUrl } from '~/stores/context';
 import { useCurrentUser, useSiteUrl } from '~/stores/context';
 
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
@@ -25,10 +26,14 @@ const AdminSlackIntegrationPage: NextPage<Props> = (props) => {
   useCurrentUser(props.currentUser ?? null);
   useCurrentUser(props.currentUser ?? null);
   useSiteUrl(props.siteUrl);
   useSiteUrl(props.siteUrl);
 
 
-  const title = t('slack_integration.slack_integration');
+  const componentTitle = t('slack_integration.slack_integration');
+  const pageTitle = generateCustomTitle(props, componentTitle);
 
 
   return (
   return (
-    <AdminLayout title={useCustomTitle(props, title)} componentTitle={title} >
+    <AdminLayout componentTitle={componentTitle}>
+      <Head>
+        <title>{pageTitle}</title>
+      </Head>
       <SlackIntegration />
       <SlackIntegration />
     </AdminLayout>
     </AdminLayout>
   );
   );

+ 7 - 3
packages/app/src/pages/admin/user-group-detail/[userGroupId].page.tsx

@@ -3,10 +3,11 @@ import {
 } from 'next';
 } from 'next';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
+import Head from 'next/head';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
 
 
 import { CrowiRequest } from '~/interfaces/crowi-request';
 import { CrowiRequest } from '~/interfaces/crowi-request';
-import { CommonProps, useCustomTitle } from '~/pages/utils/commons';
+import { CommonProps, generateCustomTitle } from '~/pages/utils/commons';
 import { useIsAclEnabled, useCurrentUser } from '~/stores/context';
 import { useIsAclEnabled, useCurrentUser } from '~/stores/context';
 import { useIsMaintenanceMode } from '~/stores/maintenanceMode';
 import { useIsMaintenanceMode } from '~/stores/maintenanceMode';
 
 
@@ -27,14 +28,17 @@ const AdminUserGroupDetailPage: NextPage<Props> = (props: Props) => {
   const { userGroupId } = router.query;
   const { userGroupId } = router.query;
 
 
   const title = t('user_group_management.user_group_management');
   const title = t('user_group_management.user_group_management');
-  const customTitle = useCustomTitle(props, title);
+  const customTitle = generateCustomTitle(props, title);
 
 
   const currentUserGroupId = Array.isArray(userGroupId) ? userGroupId[0] : userGroupId;
   const currentUserGroupId = Array.isArray(userGroupId) ? userGroupId[0] : userGroupId;
 
 
   useIsAclEnabled(props.isAclEnabled);
   useIsAclEnabled(props.isAclEnabled);
 
 
   return (
   return (
-    <AdminLayout title={customTitle} componentTitle={title} >
+    <AdminLayout componentTitle={title}>
+      <Head>
+        <title>{customTitle}</title>
+      </Head>
       {
       {
         currentUserGroupId != null && router.isReady
         currentUserGroupId != null && router.isReady
       && <UserGroupDetailPage userGroupId={currentUserGroupId} />
       && <UserGroupDetailPage userGroupId={currentUserGroupId} />

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