Parcourir la source

Merge branch 'master' into imprv/use-css-var

Yuki Takei il y a 3 ans
Parent
commit
377562a17e
100 fichiers modifiés avec 743 ajouts et 541 suppressions
  1. 0 2
      .github/workflows/ci-app.yml
  2. 0 1
      .github/workflows/ci-slackbot-proxy.yml
  3. 2 2
      .github/workflows/release.yml
  4. 9 0
      .mergify.yml
  5. 49 2
      CHANGELOG.md
  6. 1 1
      lerna.json
  7. 1 1
      package.json
  8. 1 1
      packages/app/docker/README.md
  9. 10 10
      packages/app/package.json
  10. 12 0
      packages/app/public/static/locales/en_US/translation.json
  11. 13 0
      packages/app/public/static/locales/ja_JP/translation.json
  12. 12 0
      packages/app/public/static/locales/zh_CN/translation.json
  13. 1 11
      packages/app/resource/locales/en_US/sandbox.md
  14. 0 10
      packages/app/resource/locales/ja_JP/sandbox.md
  15. 1 11
      packages/app/resource/locales/zh_CN/sandbox.md
  16. 2 3
      packages/app/src/client/services/layout.ts
  17. 1 1
      packages/app/src/components/Admin/AdminHome/SystemInfomationTable.tsx
  18. 31 42
      packages/app/src/components/Admin/Customize/CustomizeLogoSetting.tsx
  19. 33 1
      packages/app/src/components/Fab.module.scss
  20. 50 34
      packages/app/src/components/Fab.tsx
  21. 4 1
      packages/app/src/components/InAppNotification/InAppNotificationDropdown.tsx
  22. 1 1
      packages/app/src/components/Layout/BasicLayout.tsx
  23. 1 1
      packages/app/src/components/Layout/MainPane.tsx
  24. 1 1
      packages/app/src/components/Layout/ShareLinkLayout.tsx
  25. 1 1
      packages/app/src/components/Me/ApiSettings.tsx
  26. 2 2
      packages/app/src/components/Me/ProfileImageSettings.tsx
  27. 1 1
      packages/app/src/components/Navbar/AuthorInfo.tsx
  28. 16 9
      packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  29. 8 8
      packages/app/src/components/Navbar/GrowiNavbar.tsx
  30. 0 9
      packages/app/src/components/Navbar/GrowiSubNavigation.module.scss
  31. 0 138
      packages/app/src/components/Navbar/GrowiSubNavigationSwitcher.jsx
  32. 9 0
      packages/app/src/components/Navbar/GrowiSubNavigationSwitcher.module.scss
  33. 103 0
      packages/app/src/components/Navbar/GrowiSubNavigationSwitcher.tsx
  34. 17 9
      packages/app/src/components/Page.tsx
  35. 2 2
      packages/app/src/components/Page/DisplaySwitcher.tsx
  36. 5 3
      packages/app/src/components/Page/TagEditModal.jsx
  37. 3 1
      packages/app/src/components/Page/TagsInput.tsx
  38. 1 1
      packages/app/src/components/PageAlert/TrashPageAlert.tsx
  39. 10 3
      packages/app/src/components/PageEditor.tsx
  40. 6 1
      packages/app/src/components/PageEditor/DrawioModal.tsx
  41. 6 1
      packages/app/src/components/PageEditor/ScrollSyncHelper.js
  42. 12 3
      packages/app/src/components/PageEditorByHackmd.tsx
  43. 1 1
      packages/app/src/components/PrivateLegacyPages.tsx
  44. 2 2
      packages/app/src/components/SearchPage.tsx
  45. 0 0
      packages/app/src/components/SearchPage/SearchPageBase.module.scss
  46. 3 2
      packages/app/src/components/SearchPage/SearchPageBase.tsx
  47. 8 0
      packages/app/src/components/SearchPage/SearchResultContent.module.scss
  48. 3 1
      packages/app/src/components/SearchPage/SearchResultContent.tsx
  49. 1 1
      packages/app/src/components/Sidebar/RecentChanges.tsx
  50. 1 1
      packages/app/src/components/Sidebar/SidebarNav.tsx
  51. 20 20
      packages/app/src/components/StickyStretchableScroller.tsx
  52. 1 1
      packages/app/src/components/User/UserDate.jsx
  53. 34 8
      packages/app/src/pages/[[...path]].page.tsx
  54. 2 2
      packages/app/src/pages/_app.page.tsx
  55. 4 1
      packages/app/src/pages/_search.page.tsx
  56. 1 7
      packages/app/src/pages/admin/[...path].page.tsx
  57. 4 1
      packages/app/src/pages/admin/customize.page.tsx
  58. 2 1
      packages/app/src/pages/admin/index.page.tsx
  59. 3 1
      packages/app/src/pages/installer.page.tsx
  60. 3 1
      packages/app/src/pages/invited.page.tsx
  61. 3 2
      packages/app/src/pages/login/index.page.tsx
  62. 1 1
      packages/app/src/pages/me/[[...path]].page.tsx
  63. 27 7
      packages/app/src/pages/share/[[...path]].page.tsx
  64. 1 1
      packages/app/src/pages/tags.page.tsx
  65. 5 2
      packages/app/src/pages/trash.page.tsx
  66. 3 1
      packages/app/src/pages/user-activation.page.tsx
  67. 5 6
      packages/app/src/pages/utils/commons.ts
  68. 8 0
      packages/app/src/server/middlewares/certify-brand-logo.ts
  69. 6 0
      packages/app/src/server/middlewares/login-required.js
  70. 0 3
      packages/app/src/server/models/config.ts
  71. 0 14
      packages/app/src/server/routes/apiv3/customize-setting.js
  72. 10 0
      packages/app/src/server/routes/attachment.js
  73. 4 0
      packages/app/src/server/routes/index.js
  74. 11 0
      packages/app/src/server/service/attachment.js
  75. 1 1
      packages/app/src/services/renderer/rehype-plugins/add-line-number-attribute.ts
  76. 5 2
      packages/app/src/services/renderer/remark-plugins/plantuml.ts
  77. 3 3
      packages/app/src/services/renderer/renderer.tsx
  78. 8 4
      packages/app/src/stores/context.tsx
  79. 1 1
      packages/app/src/stores/editor.tsx
  80. 3 1
      packages/app/src/stores/in-app-notification.ts
  81. 23 20
      packages/app/src/stores/page.tsx
  82. 5 4
      packages/app/src/stores/personal-settings.tsx
  83. 2 2
      packages/app/src/stores/ui.tsx
  84. 5 1
      packages/app/src/stores/user.tsx
  85. 0 32
      packages/app/src/styles/_layout.scss
  86. 3 1
      packages/app/src/styles/theme/_apply-colors.scss
  87. 1 3
      packages/app/test/cypress/integration/10-install/10-install--install.spec.ts
  88. 2 2
      packages/app/test/cypress/integration/20-basic-features/20-basic-features--access-to-page.spec.ts
  89. 2 2
      packages/app/test/cypress/integration/21-basic-features-for-guest/21-basic-features-for-guest--access-to-page.spec.ts
  90. 26 32
      packages/app/test/cypress/integration/50-sidebar/50-sidebar--access-to-side-bar.spec.ts
  91. 10 4
      packages/app/test/cypress/integration/50-sidebar/50-sidebar--switching-sidebar-mode.spec.ts
  92. 2 2
      packages/app/test/cypress/integration/60-home/60-home--home.spec.ts
  93. 14 0
      packages/app/test/cypress/support/blackout.ts
  94. 6 1
      packages/app/test/cypress/support/screenshot.ts
  95. 1 1
      packages/codemirror-textlint/package.json
  96. 1 1
      packages/core/package.json
  97. 1 1
      packages/hackmd/package.json
  98. 1 1
      packages/preset-themes/package.json
  99. 1 1
      packages/remark-drawio/package.json
  100. 1 1
      packages/remark-growi-directive/package.json

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

@@ -5,8 +5,6 @@ on:
     branches-ignore:
     branches-ignore:
       - release/**
       - release/**
       - rc/**
       - rc/**
-      - chore/**
-      - support/prepare-v**
     paths:
     paths:
       - .github/workflows/ci-app.yml
       - .github/workflows/ci-app.yml
       - .eslint*
       - .eslint*

+ 0 - 1
.github/workflows/ci-slackbot-proxy.yml

@@ -5,7 +5,6 @@ on:
     branches-ignore:
     branches-ignore:
       - release/**
       - release/**
       - rc/**
       - rc/**
-      - chore/**
       - support/prepare-v**
       - support/prepare-v**
     paths:
     paths:
       - .github/workflows/ci-slackbot-proxy.yml
       - .github/workflows/ci-slackbot-proxy.yml

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

@@ -116,8 +116,8 @@ jobs:
         source_branch: support/prepare-v${{ steps.package-json.outputs.packageVersion }}
         source_branch: support/prepare-v${{ steps.package-json.outputs.packageVersion }}
         destination_branch: master
         destination_branch: master
         pr_title: Prepare v${{ steps.package-json.outputs.packageVersion }}
         pr_title: Prepare v${{ steps.package-json.outputs.packageVersion }}
-        pr_label: exclude from changelog
-        pr_body: "An automated PR generated by ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
+        pr_label: exclude from changelog,prepare next version
+        pr_body: "An automated PR generated by create-pr-for-next-rc"
         github_token: ${{ secrets.GITHUB_TOKEN }}
         github_token: ${{ secrets.GITHUB_TOKEN }}
 
 
 
 

+ 9 - 0
.mergify.yml

@@ -11,3 +11,12 @@ pull_request_rules:
     actions:
     actions:
       merge:
       merge:
         method: merge
         method: merge
+
+  - name: Automatic merge for Preparing next version
+    conditions:
+      - author = github-actions[bot]
+      - '#approved-reviews-by >= 1'
+      - label = "prepare next version"
+    actions:
+      merge:
+        method: merge

+ 49 - 2
CHANGELOG.md

@@ -1,10 +1,49 @@
 # Changelog
 # Changelog
 
 
-## [Unreleased](https://github.com/weseek/growi/compare/v6.0.0...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v6.0.2...HEAD)
 
 
 *Please do not manually update this file. We've automated the process.*
 *Please do not manually update this file. We've automated the process.*
 
 
-## [v6.0.0](https://github.com/weseek/growi/compare/v5.1.7...v6.0.0) - 2022-12-27
+## [v6.0.2](https://github.com/weseek/growi/compare/v6.0.1...v6.0.2) - 2023-01-10
+
+### 🐛 Bug Fixes
+
+- fix: Attaching page title as initial header section (#7228) @yukendev
+- fix: Update PageTree data after saving page (#7227) @yukendev
+- fix: Lsx "filter" and "except" options does not work (#7226) @yuki-takei
+- fix: Omit remark-growi-directive shortcuts (#7225) @yuki-takei
+
+### 🧰 Maintenance
+
+- ci(deps-dev): bump textlint-rule-no-doubled-joshi from 4.0.0 to 4.0.1 (#7222) @dependabot
+
+## [v6.0.1](https://github.com/weseek/growi/compare/v6.0.0...v6.0.1) - 2023-01-07
+
+### 🚀 Improvement
+
+- imprv: Reduce frequent API calling by SWR (#7218) @yuki-takei
+- imprv: Do not use api for fetching pages when using shared pages (#7213) @miya
+
+### 🐛 Bug Fixes
+
+- fix: Custom logo not displayed on shared page (#7205) @miya
+- fix: Attach i18n User Setting and TagEditModal (#7216) @jam411
+- fix: Make PLANTUML_URI v5.x compatible (#7215) @yuki-takei
+- fix: Launch with PROMSTER_ENABLED=true failed (#7210) @yuki-takei
+- fix: Lsx performs with strange behavior (#7209) @yuki-takei
+
+### 🧰 Maintenance
+
+- support: Arm architecture (#7212) @yuki-takei
+- ci(deps): bump anothrNick/github-tag-action from 1.38.0 to 1.56.0 (#7195) @dependabot
+- ci(deps): bump google-github-actions/setup-gcloud from 0 to 1 (#7193) @dependabot
+- ci(deps): bump github/codeql-action from 1 to 2 (#7194) @dependabot
+- ci(deps): bump flat from 5.0.0 to 5.0.2 (#7200) @dependabot
+- ci(deps): bump json5 from 1.0.1 to 1.0.2 (#7201) @dependabot
+- ci(Mergify): configuration update (#7202) @yuki-takei
+- support: Uninstall swig-template (#7192) @yuki-takei
+
+## [v6.0.0](https://github.com/weseek/growi/compare/v5.1.8...v6.0.0) - 2022-12-27
 
 
 ### 💎 Features
 ### 💎 Features
 
 
@@ -17,6 +56,14 @@
 - support: Request scoped SWR (#6742) @yuki-takei
 - support: Request scoped SWR (#6742) @yuki-takei
 - support: Build preset themes within external package (#7057) @yuki-takei
 - support: Build preset themes within external package (#7057) @yuki-takei
 
 
+## [v5.1.8](https://github.com/weseek/growi/compare/v5.1.7...v5.1.8) - 2022-11-17
+
+### 🐛 Bug Fixes
+
+- fix: Put back page from trash (#6835) @yukendev
+- fix: Updating page content width is not working (#6914) @yukendev
+- fix: Create page at installer (#6930) @hakumizuki @yuki-takei
+
 ## [v5.1.7](https://github.com/weseek/growi/compare/v5.1.6...v5.1.7) - 2022-10-26
 ## [v5.1.7](https://github.com/weseek/growi/compare/v5.1.6...v5.1.7) - 2022-10-26
 
 
 ### 🐛 Bug Fixes
 ### 🐛 Bug Fixes

+ 1 - 1
lerna.json

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

+ 1 - 1
package.json

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

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

@@ -10,7 +10,7 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 ------------------------------------------------
 
 
-* [`6.0.0`, `6.0`, `6`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v6.0.0/packages/app/docker/Dockerfile)
+* [`6.0.2`, `6.0`, `6`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v6.0.2/packages/app/docker/Dockerfile)
 * [`5.1.7`, `5.1`, `5`](https://github.com/weseek/growi/blob/v5.1.7/packages/app/docker/Dockerfile)
 * [`5.1.7`, `5.1`, `5`](https://github.com/weseek/growi/blob/v5.1.7/packages/app/docker/Dockerfile)
 * [`5.1.7-nocdn`, `5.1-nocdn`, `5-nocdn`](https://github.com/weseek/growi/blob/v5.1.7/packages/app/docker/Dockerfile)
 * [`5.1.7-nocdn`, `5.1-nocdn`, `5-nocdn`](https://github.com/weseek/growi/blob/v5.1.7/packages/app/docker/Dockerfile)
 * [`4.5.23`, `4.5`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.23/packages/app/docker/Dockerfile)
 * [`4.5.23`, `4.5`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.23/packages/app/docker/Dockerfile)

+ 10 - 10
packages/app/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/app",
   "name": "@growi/app",
-  "version": "6.0.1-RC.0",
+  "version": "6.0.3-RC.0",
   "license": "MIT",
   "license": "MIT",
   "scripts": {
   "scripts": {
     "//// for production": "",
     "//// for production": "",
@@ -66,14 +66,14 @@
     "@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.1-RC.0",
-    "@growi/core": "^6.0.1-RC.0",
-    "@growi/hackmd": "^6.0.1-RC.0",
-    "@growi/preset-themes": "^6.0.1-RC.0",
-    "@growi/remark-drawio": "^6.0.1-RC.0",
-    "@growi/remark-growi-directive": "^6.0.1-RC.0",
-    "@growi/remark-lsx": "^6.0.1-RC.0",
-    "@growi/slack": "^6.0.1-RC.0",
+    "@growi/codemirror-textlint": "^6.0.3-RC.0",
+    "@growi/core": "^6.0.3-RC.0",
+    "@growi/hackmd": "^6.0.3-RC.0",
+    "@growi/preset-themes": "^6.0.3-RC.0",
+    "@growi/remark-drawio": "^6.0.3-RC.0",
+    "@growi/remark-growi-directive": "^6.0.3-RC.0",
+    "@growi/remark-lsx": "^6.0.3-RC.0",
+    "@growi/slack": "^6.0.3-RC.0",
     "@promster/express": "^7.0.6",
     "@promster/express": "^7.0.6",
     "@promster/server": "^7.0.8",
     "@promster/server": "^7.0.8",
     "@slack/web-api": "^6.2.4",
     "@slack/web-api": "^6.2.4",
@@ -200,7 +200,7 @@
     "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.1-RC.0",
+    "@growi/ui": "^6.0.3-RC.0",
     "@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",

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

@@ -85,6 +85,7 @@
   "No diff": "No diff",
   "No diff": "No diff",
   "User ID": "User ID",
   "User ID": "User ID",
   "User Information": "User information",
   "User Information": "User information",
+  "User Activation": "User Activation",
   "Basic Info": "Basic info",
   "Basic Info": "Basic info",
   "Name": "Name",
   "Name": "Name",
   "Email": "Email",
   "Email": "Email",
@@ -164,6 +165,7 @@
     "no_page_list": "There are no pages under this page."
     "no_page_list": "There are no pages under this page."
   },
   },
   "installer": {
   "installer": {
+    "title": "Installer",
     "setup": "Setup",
     "setup": "Setup",
     "create_initial_account": "Create an initial account",
     "create_initial_account": "Create an initial account",
     "initial_account_will_be_administrator_automatically": "The initial account will be administrator automatically.",
     "initial_account_will_be_administrator_automatically": "The initial account will be administrator automatically.",
@@ -551,6 +553,7 @@
     "popover_desc": "Input channel name. You can notify multiple channels by entering a comma-separated list."
     "popover_desc": "Input channel name. You can notify multiple channels by entering a comma-separated list."
   },
   },
   "search_result": {
   "search_result": {
+    "title": "Search",
     "result_meta": "Search results for:",
     "result_meta": "Search results for:",
     "deletion_mode_btn_lavel": "Select and delete page",
     "deletion_mode_btn_lavel": "Select and delete page",
     "cancel": "Cancel",
     "cancel": "Cancel",
@@ -606,6 +609,7 @@
     }
     }
   },
   },
   "login": {
   "login": {
+    "title": "Login",
     "sign_in_error": "Login error",
     "sign_in_error": "Login error",
     "registration_successful": "registration_successful. Please wait for administrator approval.",
     "registration_successful": "registration_successful. Please wait for administrator approval.",
     "Setup": "Setup",
     "Setup": "Setup",
@@ -613,6 +617,7 @@
     "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)"
   },
   },
   "invited": {
   "invited": {
+    "title": "Invited",
     "discription_heading": "Create Account",
     "discription_heading": "Create Account",
     "discription": "Create an your account with the invited email address"
     "discription": "Create an your account with the invited email address"
   },
   },
@@ -798,5 +803,12 @@
   "v5_page_migration": {
   "v5_page_migration": {
     "page_tree_not_avaliable" : "Page tree feature is not available yet.",
     "page_tree_not_avaliable" : "Page tree feature is not available yet.",
     "go_to_settings": "Go to settings to enable the feature"
     "go_to_settings": "Go to settings to enable the feature"
+  },
+  "tag_edit_modal": {
+    "edit_tags": "Edit Tags",
+    "done": "Done",
+    "tags_input": {
+      "tag_name": "tag name"
+    }
   }
   }
 }
 }

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

@@ -80,7 +80,9 @@
   "View diff": "差分を表示",
   "View diff": "差分を表示",
   "No diff": "差分なし",
   "No diff": "差分なし",
   "User ID": "ユーザーID",
   "User ID": "ユーザーID",
+  "User Settings": "ユーザー設定",
   "User Information": "ユーザー情報",
   "User Information": "ユーザー情報",
+  "User Activation": "ユーザーアクティベーション",
   "Basic Info": "ユーザーの基本情報",
   "Basic Info": "ユーザーの基本情報",
   "Name": "名前",
   "Name": "名前",
   "Email": "メールアドレス",
   "Email": "メールアドレス",
@@ -165,6 +167,7 @@
     "no_page_list": "このページの配下にはページが存在しません。"
     "no_page_list": "このページの配下にはページが存在しません。"
   },
   },
   "installer": {
   "installer": {
+    "title": "インストーラー",
     "setup": "セットアップ",
     "setup": "セットアップ",
     "create_initial_account": "最初のアカウントの作成",
     "create_initial_account": "最初のアカウントの作成",
     "initial_account_will_be_administrator_automatically": "初めに作成するアカウントは、自動的に管理者権限が付与されます",
     "initial_account_will_be_administrator_automatically": "初めに作成するアカウントは、自動的に管理者権限が付与されます",
@@ -550,6 +553,7 @@
     "popover_desc": "チャンネル名を入れてください。カンマ区切りのリストを入力することで複数のチャンネルに通知することができます。"
     "popover_desc": "チャンネル名を入れてください。カンマ区切りのリストを入力することで複数のチャンネルに通知することができます。"
   },
   },
   "search_result": {
   "search_result": {
+    "title": "検索",
     "result_meta": "検索結果:",
     "result_meta": "検索結果:",
     "deletion_mode_btn_lavel": "ページを指定して削除",
     "deletion_mode_btn_lavel": "ページを指定して削除",
     "cancel": "キャンセル",
     "cancel": "キャンセル",
@@ -605,6 +609,7 @@
     }
     }
   },
   },
   "login": {
   "login": {
+    "title": "ログイン",
     "sign_in_error": "ログインエラー",
     "sign_in_error": "ログインエラー",
     "registration_successful": "登録が完了しました。管理者の承認をお待ちください。",
     "registration_successful": "登録が完了しました。管理者の承認をお待ちください。",
     "Setup": "セットアップ",
     "Setup": "セットアップ",
@@ -612,6 +617,7 @@
     "set_env_var_for_logs": "(ログを取得するためには、環境変数 <code>DEBUG=crowi:service:PassportService</code> を設定してください。)"
     "set_env_var_for_logs": "(ログを取得するためには、環境変数 <code>DEBUG=crowi:service:PassportService</code> を設定してください。)"
   },
   },
   "invited": {
   "invited": {
+    "title": "招待",
     "discription_heading": "アカウント作成",
     "discription_heading": "アカウント作成",
     "discription": "招待を受け取ったメールアドレスでアカウントを作成します"
     "discription": "招待を受け取ったメールアドレスでアカウントを作成します"
   },
   },
@@ -797,5 +803,12 @@
   "v5_page_migration": {
   "v5_page_migration": {
     "page_tree_not_avaliable" : "Page Tree 機能は現在使用できません。",
     "page_tree_not_avaliable" : "Page Tree 機能は現在使用できません。",
     "go_to_settings": "設定する"
     "go_to_settings": "設定する"
+  },
+  "tag_edit_modal": {
+    "edit_tags": "タグの編集",
+    "done": "完了",
+    "tags_input": {
+      "tag_name": "タグ名"
+    }
   }
   }
 }
 }

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

@@ -86,6 +86,7 @@
 	"My Drafts": "My Drafts",
 	"My Drafts": "My Drafts",
 	"User Settings": "用户设置",
 	"User Settings": "用户设置",
 	"User Information": "用户信息",
 	"User Information": "用户信息",
+  "User Activation": "用户激活",
 	"Basic Info": "基础信息",
 	"Basic Info": "基础信息",
 	"Name": "姓名",
 	"Name": "姓名",
 	"Email": "邮箱",
 	"Email": "邮箱",
@@ -171,6 +172,7 @@
     "no_page_list": "There are no pages under this page."
     "no_page_list": "There are no pages under this page."
   },
   },
 	"installer": {
 	"installer": {
+    "title": "安装",
 		"setup": "安装",
 		"setup": "安装",
 		"create_initial_account": "创建初始用户",
 		"create_initial_account": "创建初始用户",
 		"initial_account_will_be_administrator_automatically": "初始帐户将自动成为管理员。",
 		"initial_account_will_be_administrator_automatically": "初始帐户将自动成为管理员。",
@@ -555,6 +557,7 @@
     "link_sharing_is_disabled": "链接共享已被禁用"
     "link_sharing_is_disabled": "链接共享已被禁用"
   },
   },
 	"search_result": {
 	"search_result": {
+    "title": "搜索",
 		"result_meta": "搜索结果:",
 		"result_meta": "搜索结果:",
 		"deletion_mode_btn_lavel": "选择并删除页面",
 		"deletion_mode_btn_lavel": "选择并删除页面",
 		"cancel": "取消",
 		"cancel": "取消",
@@ -610,6 +613,7 @@
     }
     }
   },
   },
 	"login": {
 	"login": {
+    "title": "登录",
 		"sign_in_error": "登录错误",
 		"sign_in_error": "登录错误",
 		"registration_successful": "注册成功。请等待管理员批准",
 		"registration_successful": "注册成功。请等待管理员批准",
 		"Setup": "安装程序",
 		"Setup": "安装程序",
@@ -617,6 +621,7 @@
     "set_env_var_for_logs": "(请设置环境变量 <code>DEBUG=crowi:service:PassportService</code> 以获得日志。)"
     "set_env_var_for_logs": "(请设置环境变量 <code>DEBUG=crowi:service:PassportService</code> 以获得日志。)"
 	},
 	},
   "invited": {
   "invited": {
+    "invited": "邀请函",
     "discription_heading": "创建账户",
     "discription_heading": "创建账户",
     "discription": "用被邀请的电子邮件地址创建一个你的账户"
     "discription": "用被邀请的电子邮件地址创建一个你的账户"
   },
   },
@@ -802,5 +807,12 @@
   "v5_page_migration": {
   "v5_page_migration": {
     "page_tree_not_avaliable": "Page Tree 功能不可用",
     "page_tree_not_avaliable": "Page Tree 功能不可用",
     "go_to_settings": "进入设置,启用该功能"
     "go_to_settings": "进入设置,启用该功能"
+  },
+  "tag_edit_modal": {
+    "edit_tags": "编辑标签",
+    "done": "完毕",
+    "tags_input": {
+      "tag_name": "标签名称"
+    }
   }
   }
 }
 }

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

@@ -233,16 +233,6 @@ You can create links using `[Display text](URL)`.
 
 
 [Google](https://www.google.co.jp/)
 [Google](https://www.google.co.jp/)
 
 
-## Crowi compatibility
-
-```
-[/Sandbox]
-</user/admin1>
-```
-
-[/Sandbox]  
-</user/admin1>
-
 ## Pukiwiki like linker
 ## Pukiwiki like linker
 
 
 This is the most flexible linker.
 This is the most flexible linker.
@@ -254,7 +244,7 @@ Example of Bootstrap4 is [[here>./Bootstrap4]]
 ```
 ```
 
 
 [[./Bootstrap4]]  
 [[./Bootstrap4]]  
-Example of Bootstrap4 is[[here>./Bootstrap4]]
+Example of Bootstrap4 is [[here>./Bootstrap4]]
 
 
 # :memo: Lists
 # :memo: Lists
 
 

+ 0 - 10
packages/app/resource/locales/ja_JP/sandbox.md

@@ -232,16 +232,6 @@ ___
 
 
 [Google](https://www.google.co.jp/)
 [Google](https://www.google.co.jp/)
 
 
-## Crowi 互換
-
-```
-[/Sandbox]
-</user/admin1>
-```
-
-[/Sandbox]  
-</user/admin1>
-
 ## Pukiwiki like linker
 ## Pukiwiki like linker
 
 
 最も柔軟な Linker です。
 最も柔軟な Linker です。

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

@@ -233,16 +233,6 @@ You can create links using `[Display text](URL)`.
 
 
 [Google](https://www.google.co.jp/)
 [Google](https://www.google.co.jp/)
 
 
-## Crowi compatibility
-
-```
-[/Sandbox]
-</user/admin1>
-```
-
-[/Sandbox]  
-</user/admin1>
-
 ## Pukiwiki like linker
 ## Pukiwiki like linker
 
 
 This is the most flexible linker.
 This is the most flexible linker.
@@ -250,7 +240,7 @@ Both the page description and link address can be displayed on the page.
 
 
 ```
 ```
 [[./Bootstrap4]]
 [[./Bootstrap4]]
-Example of Bootstrap4 is[[here>./Bootstrap4]]
+Example of Bootstrap4 is [[here>./Bootstrap4]]
 ```
 ```
 
 
 [[./Bootstrap4]]  
 [[./Bootstrap4]]  

+ 2 - 3
packages/app/src/client/services/layout.ts

@@ -1,4 +1,4 @@
-import { useIsContainerFluid, useShareLinkId } from '~/stores/context';
+import { useIsContainerFluid } from '~/stores/context';
 import { useSWRxCurrentPage } from '~/stores/page';
 import { useSWRxCurrentPage } from '~/stores/page';
 import { useEditorMode } from '~/stores/ui';
 import { useEditorMode } from '~/stores/ui';
 
 
@@ -17,8 +17,7 @@ export const useEditorModeClassName = (): string => {
 };
 };
 
 
 export const useCurrentGrowiLayoutFluidClassName = (): string => {
 export const useCurrentGrowiLayoutFluidClassName = (): string => {
-  const { data: shareLinkId } = useShareLinkId();
-  const { data: currentPage } = useSWRxCurrentPage(shareLinkId ?? undefined);
+  const { data: currentPage } = useSWRxCurrentPage();
 
 
   const { data: dataIsContainerFluid } = useIsContainerFluid();
   const { data: dataIsContainerFluid } = useIsContainerFluid();
 
 

+ 1 - 1
packages/app/src/components/Admin/AdminHome/SystemInfomationTable.tsx

@@ -25,7 +25,7 @@ const SystemInformationTable = (props: Props) => {
       <tbody>
       <tbody>
         <tr>
         <tr>
           <th>GROWI</th>
           <th>GROWI</th>
-          <td data-hide-in-vrt>{ growiVersion }</td>
+          <td data-vrt-blackout>{ growiVersion }</td>
         </tr>
         </tr>
         <tr>
         <tr>
           <th>node.js</th>
           <th>node.js</th>

+ 31 - 42
packages/app/src/components/Admin/Customize/CustomizeLogoSetting.tsx

@@ -1,45 +1,34 @@
-import React, { useCallback, useEffect, useState } from 'react';
+import React, { useCallback, useMemo, useState } from 'react';
 
 
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
 import { toastError, toastSuccess } from '~/client/util/apiNotification';
 import { toastError, toastSuccess } from '~/client/util/apiNotification';
 import {
 import {
-  apiv3Delete, apiv3Get, apiv3PostForm, apiv3Put,
+  apiv3Delete, apiv3PostForm, apiv3Put,
 } from '~/client/util/apiv3-client';
 } from '~/client/util/apiv3-client';
 import ImageCropModal from '~/components/Common/ImageCropModal';
 import ImageCropModal from '~/components/Common/ImageCropModal';
+import { useIsDefaultLogo, useIsCustomizedLogoUploaded } from '~/stores/context';
 
 
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 
+
 const DEFAULT_LOGO = '/images/logo.svg';
 const DEFAULT_LOGO = '/images/logo.svg';
+const CUSTOMIZED_LOGO = '/attachment/brand-logo';
 
 
 const CustomizeLogoSetting = (): JSX.Element => {
 const CustomizeLogoSetting = (): JSX.Element => {
 
 
   const { t } = useTranslation();
   const { t } = useTranslation();
+  const { data: isDefaultLogo } = useIsDefaultLogo();
+  const { data: isCustomizedLogoUploaded, mutate: mutateIsCustomizedLogoUploaded } = useIsCustomizedLogoUploaded();
 
 
   const [uploadLogoSrc, setUploadLogoSrc] = useState<ArrayBuffer | string | null>(null);
   const [uploadLogoSrc, setUploadLogoSrc] = useState<ArrayBuffer | string | null>(null);
   const [isImageCropModalShow, setIsImageCropModalShow] = useState<boolean>(false);
   const [isImageCropModalShow, setIsImageCropModalShow] = useState<boolean>(false);
-  const [isDefaultLogo, setIsDefaultLogo] = useState<boolean>(true);
+  const [isDefaultLogoSelected, setIsDefaultLogoSelected] = useState<boolean>(isDefaultLogo ?? true);
   const [retrieveError, setRetrieveError] = useState<any>();
   const [retrieveError, setRetrieveError] = useState<any>();
-  const [customizedLogoSrc, setCustomizedLogoSrc] = useState< string | null >(null);
-
-  const retrieveData = useCallback(async() => {
-    try {
-      const response = await apiv3Get('/customize-setting/customize-logo');
-      const { isDefaultLogo: _isDefaultLogo, customizedLogoSrc } = response.data;
-      const isDefaultLogo = _isDefaultLogo ?? true;
-
-      setIsDefaultLogo(isDefaultLogo);
-      setCustomizedLogoSrc(customizedLogoSrc);
-    }
-    catch (err) {
-      setRetrieveError(err);
-      throw new Error('Failed to fetch data');
-    }
-  }, []);
 
 
-  useEffect(() => {
-    retrieveData();
-  }, [retrieveData]);
+  const currentLogo = useMemo(() => {
+    return isDefaultLogo ? DEFAULT_LOGO : CUSTOMIZED_LOGO;
+  }, [isDefaultLogo]);
 
 
   const onSelectFile = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
   const onSelectFile = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
     if (e.target.files != null && e.target.files.length > 0) {
     if (e.target.files != null && e.target.files.length > 0) {
@@ -52,22 +41,18 @@ const CustomizeLogoSetting = (): JSX.Element => {
 
 
   const onClickSubmit = useCallback(async() => {
   const onClickSubmit = useCallback(async() => {
     try {
     try {
-      const response = await apiv3Put('/customize-setting/customize-logo', {
-        isDefaultLogo,
-      });
-      const { customizedParams } = response.data;
-      setIsDefaultLogo(customizedParams.isDefaultLogo);
+      await apiv3Put('/customize-setting/customize-logo', { isDefaultLogo: isDefaultLogoSelected });
       toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_logo'), ns: 'commons' }));
       toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_logo'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
     }
     }
-  }, [t, isDefaultLogo]);
+  }, [t, isDefaultLogoSelected]);
 
 
   const onClickDeleteBtn = useCallback(async() => {
   const onClickDeleteBtn = useCallback(async() => {
     try {
     try {
       await apiv3Delete('/customize-setting/delete-brand-logo');
       await apiv3Delete('/customize-setting/delete-brand-logo');
-      setCustomizedLogoSrc(null);
+      mutateIsCustomizedLogoUploaded(false);
       toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.current_logo'), ns: 'commons' }));
       toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.current_logo'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
@@ -75,15 +60,15 @@ const CustomizeLogoSetting = (): JSX.Element => {
       setRetrieveError(err);
       setRetrieveError(err);
       throw new Error('Failed to delete logo');
       throw new Error('Failed to delete logo');
     }
     }
-  }, [t]);
+  }, [mutateIsCustomizedLogoUploaded, t]);
 
 
 
 
   const processImageCompletedHandler = useCallback(async(croppedImage) => {
   const processImageCompletedHandler = useCallback(async(croppedImage) => {
     try {
     try {
       const formData = new FormData();
       const formData = new FormData();
       formData.append('file', croppedImage);
       formData.append('file', croppedImage);
-      const { data } = await apiv3PostForm('/customize-setting/upload-brand-logo', formData);
-      setCustomizedLogoSrc(data.attachment.filePathProxied);
+      await apiv3PostForm('/customize-setting/upload-brand-logo', formData);
+      mutateIsCustomizedLogoUploaded(true);
       toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.current_logo'), ns: 'commons' }));
       toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.current_logo'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
@@ -91,7 +76,7 @@ const CustomizeLogoSetting = (): JSX.Element => {
       setRetrieveError(err);
       setRetrieveError(err);
       throw new Error('Failed to upload brand logo');
       throw new Error('Failed to upload brand logo');
     }
     }
-  }, [t]);
+  }, [mutateIsCustomizedLogoUploaded, t]);
 
 
   return (
   return (
     <React.Fragment>
     <React.Fragment>
@@ -109,8 +94,8 @@ const CustomizeLogoSetting = (): JSX.Element => {
                       className="custom-control-input"
                       className="custom-control-input"
                       form="formImageType"
                       form="formImageType"
                       name="imagetypeForm[isDefaultLogo]"
                       name="imagetypeForm[isDefaultLogo]"
-                      checked={isDefaultLogo}
-                      onChange={() => { setIsDefaultLogo(true) }}
+                      checked={isDefaultLogoSelected}
+                      onChange={() => { setIsDefaultLogoSelected(true) }}
                     />
                     />
                     <label className="custom-control-label" htmlFor="radioDefaultLogo">
                     <label className="custom-control-label" htmlFor="radioDefaultLogo">
                       {t('admin:customize_settings.default_logo')}
                       {t('admin:customize_settings.default_logo')}
@@ -128,8 +113,8 @@ const CustomizeLogoSetting = (): JSX.Element => {
                       className="custom-control-input"
                       className="custom-control-input"
                       form="formImageType"
                       form="formImageType"
                       name="imagetypeForm[isDefaultLogo]"
                       name="imagetypeForm[isDefaultLogo]"
-                      checked={!isDefaultLogo}
-                      onChange={() => { setIsDefaultLogo(false) }}
+                      checked={!isDefaultLogoSelected}
+                      onChange={() => { setIsDefaultLogoSelected(false) }}
                     />
                     />
                     <label className="custom-control-label" htmlFor="radioUploadLogo">
                     <label className="custom-control-label" htmlFor="radioUploadLogo">
                       { t('admin:customize_settings.upload_logo') }
                       { t('admin:customize_settings.upload_logo') }
@@ -141,11 +126,15 @@ const CustomizeLogoSetting = (): JSX.Element => {
                     { t('admin:customize_settings.current_logo') }
                     { t('admin:customize_settings.current_logo') }
                   </label>
                   </label>
                   <div className="col-sm-8 col-12">
                   <div className="col-sm-8 col-12">
-                    <p><img src={customizedLogoSrc || DEFAULT_LOGO} className="picture picture-lg " id="settingBrandLogo" width="64" /></p>
-                    {(customizedLogoSrc != null) && (
-                      <button type="button" className="btn btn-danger" onClick={onClickDeleteBtn}>
-                        { t('admin:customize_settings.delete_logo') }
-                      </button>
+                    {isCustomizedLogoUploaded && (
+                      <>
+                        <p>
+                          <img src='/attachment/brand-logo' className="picture picture-lg " id="settingBrandLogo" width="64" />
+                        </p>
+                        <button type="button" className="btn btn-danger" onClick={onClickDeleteBtn}>
+                          { t('admin:customize_settings.delete_logo') }
+                        </button>
+                      </>
                     )}
                     )}
                   </div>
                   </div>
                 </div>
                 </div>

+ 33 - 1
packages/app/src/components/Fab.module.scss

@@ -1,9 +1,41 @@
+@use '~/styles/bootstrap/init' as bs;
+
 .grw-fab :global {
 .grw-fab :global {
+  position: fixed;
+  right: 1.5rem;
+  bottom: 3rem;
+  z-index: bs.$zindex-fixed;
+
+  transition: all 200ms linear;
+
+  .btn-create-page {
+    width: 60px;
+    height: 60px;
+    font-size: 24px;
+
+    box-shadow: 2px 3px 6px #0000005d;
+    svg {
+      width: 28px;
+      height: 28px;
+    }
+  }
+
+  .btn-scroll-to-top {
+    width: 40px;
+    height: 40px;
+
+    opacity: 0.4;
+    svg {
+      width: 18px;
+      height: 18px;
+    }
+  }
+
   // workaround
   // workaround
   // https://stackoverflow.com/a/57667536
   // https://stackoverflow.com/a/57667536
   .fadeInUp {
   .fadeInUp {
     & :local {
     & :local {
-      animation: fab-fadeinup 1s ease 0s;
+      animation: fab-fadeinup 0.5s ease 0s;
     }
     }
   }
   }
   .fadeOut {
   .fadeOut {

+ 50 - 34
packages/app/src/components/Fab.tsx

@@ -1,5 +1,5 @@
 import React, {
 import React, {
-  useState, useCallback, useRef,
+  useState, useCallback, useRef, useEffect,
 } from 'react';
 } from 'react';
 
 
 import { animateScroll } from 'react-scroll';
 import { animateScroll } from 'react-scroll';
@@ -7,9 +7,9 @@ import { useRipple } from 'react-use-ripple';
 import StickyEvents from 'sticky-events';
 import StickyEvents from 'sticky-events';
 
 
 import { DEFAULT_AUTO_SCROLL_OPTS } from '~/client/util/smooth-scroll';
 import { DEFAULT_AUTO_SCROLL_OPTS } from '~/client/util/smooth-scroll';
-import { useCurrentUser } from '~/stores/context';
 import { usePageCreateModal } from '~/stores/modal';
 import { usePageCreateModal } from '~/stores/modal';
 import { useCurrentPagePath } from '~/stores/page';
 import { useCurrentPagePath } from '~/stores/page';
+import { useIsAbleToChangeEditorMode } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { CreatePageIcon } from './Icons/CreatePageIcon';
 import { CreatePageIcon } from './Icons/CreatePageIcon';
@@ -21,45 +21,61 @@ const logger = loggerFactory('growi:cli:Fab');
 
 
 export const Fab = (): JSX.Element => {
 export const Fab = (): JSX.Element => {
 
 
-  const { data: currentUser } = useCurrentUser();
+  const { data: isAbleToChangeEditorMode } = useIsAbleToChangeEditorMode();
   const { data: currentPath = '' } = useCurrentPagePath();
   const { data: currentPath = '' } = useCurrentPagePath();
   const { open: openCreateModal } = usePageCreateModal();
   const { open: openCreateModal } = usePageCreateModal();
 
 
-  const [animateClasses, setAnimateClasses] = useState('invisible');
-  const [buttonClasses, setButtonClasses] = useState('');
+  const [animateClasses, setAnimateClasses] = useState<string>('invisible');
+  const [buttonClasses, setButtonClasses] = useState<string>('');
+  const [isSticky, setIsSticky] = useState<boolean>(false);
 
 
   // ripple
   // ripple
   const createBtnRef = useRef(null);
   const createBtnRef = useRef(null);
   useRipple(createBtnRef, { rippleColor: 'rgba(255, 255, 255, 0.3)' });
   useRipple(createBtnRef, { rippleColor: 'rgba(255, 255, 255, 0.3)' });
 
 
-  /*
-  * TODO: Comment out to prevent err >>> TypeError: Cannot read properties of null (reading 'bottom')
-  *       We need add style={{ position: 'relative }} to child elements if disable StickyEvents. see: use grep = "<Fab".
-  */
-  // const stickyChangeHandler = useCallback((event) => {
-  //   logger.debug('StickyEvents.CHANGE detected');
-
-  //   const newAnimateClasses = event.detail.isSticky ? 'animated fadeInUp faster' : 'animated fadeOut faster';
-  //   const newButtonClasses = event.detail.isSticky ? '' : 'disabled grw-pointer-events-none';
-
-  //   setAnimateClasses(newAnimateClasses);
-  //   setButtonClasses(newButtonClasses);
-  // }, []);
-
-  // // setup effect by sticky event
-  // useEffect(() => {
-  //   // sticky
-  //   // See: https://github.com/ryanwalters/sticky-events
-  //   const stickyEvents = new StickyEvents({ stickySelector: '#grw-fav-sticky-trigger' });
-  //   const { stickySelector } = stickyEvents;
-  //   const elem = document.querySelector(stickySelector);
-  //   elem.addEventListener(StickyEvents.CHANGE, stickyChangeHandler);
-
-  //   // return clean up handler
-  //   return () => {
-  //     elem.removeEventListener(StickyEvents.CHANGE, stickyChangeHandler);
-  //   };
-  // }, [stickyChangeHandler]);
+  /**
+   * After the fade animation is finished, fix the button display status.
+   * Prevents the fade animation occurred each time by button components rendered.
+   * Check Fab.module.scss for fade animation time.
+   */
+  useEffect(() => {
+    const timer = setTimeout(() => {
+      if (isSticky) {
+        setAnimateClasses('visible');
+        setButtonClasses('');
+      }
+      else {
+        setAnimateClasses('invisible');
+      }
+    }, 500);
+    return () => clearTimeout(timer);
+  }, [isSticky]);
+
+  const stickyChangeHandler = useCallback((event) => {
+    logger.debug('StickyEvents.CHANGE detected');
+
+    const newAnimateClasses = event.detail.isSticky ? 'animated fadeInUp faster' : 'animated fadeOut faster';
+    const newButtonClasses = event.detail.isSticky ? '' : 'disabled grw-pointer-events-none';
+
+    setAnimateClasses(newAnimateClasses);
+    setButtonClasses(newButtonClasses);
+    setIsSticky(event.detail.isSticky);
+  }, []);
+
+  // setup effect by sticky event
+  useEffect(() => {
+    // sticky
+    // See: https://github.com/ryanwalters/sticky-events
+    const stickyEvents = new StickyEvents({ stickySelector: '#grw-fav-sticky-trigger' });
+    const { stickySelector } = stickyEvents;
+    const elem = document.querySelector(stickySelector);
+    elem.addEventListener(StickyEvents.CHANGE, stickyChangeHandler);
+
+    // return clean up handler
+    return () => {
+      elem.removeEventListener(StickyEvents.CHANGE, stickyChangeHandler);
+    };
+  }, [stickyChangeHandler]);
 
 
   const PageCreateButton = useCallback(() => {
   const PageCreateButton = useCallback(() => {
     return (
     return (
@@ -102,7 +118,7 @@ export const Fab = (): JSX.Element => {
 
 
   return (
   return (
     <div className={`${styles['grw-fab']} grw-fab d-none d-md-block d-edit-none`} data-testid="grw-fab-container">
     <div className={`${styles['grw-fab']} grw-fab d-none d-md-block d-edit-none`} data-testid="grw-fab-container">
-      {currentUser != null && <PageCreateButton />}
+      {isAbleToChangeEditorMode && <PageCreateButton />}
       <ScrollToTopButton />
       <ScrollToTopButton />
     </div>
     </div>
   );
   );

+ 4 - 1
packages/app/src/components/InAppNotification/InAppNotificationDropdown.tsx

@@ -26,7 +26,10 @@ export const InAppNotificationDropdown = (): JSX.Element => {
   const limit = 6;
   const limit = 6;
 
 
   const { data: socket } = useDefaultSocket();
   const { data: socket } = useDefaultSocket();
-  const { data: inAppNotificationData, mutate: mutateInAppNotificationData } = useSWRxInAppNotifications(limit);
+  const { data: inAppNotificationData, mutate: mutateInAppNotificationData } = useSWRxInAppNotifications(
+    limit, undefined, undefined,
+    { revalidateOnFocus: isOpen },
+  );
   const { data: inAppNotificationUnreadStatusCount, mutate: mutateInAppNotificationUnreadStatusCount } = useSWRxInAppNotificationStatus();
   const { data: inAppNotificationUnreadStatusCount, mutate: mutateInAppNotificationUnreadStatusCount } = useSWRxInAppNotificationStatus();
 
 
   // ripple
   // ripple

+ 1 - 1
packages/app/src/components/Layout/BasicLayout.tsx

@@ -43,7 +43,7 @@ export const BasicLayout = ({ children, className }: Props): JSX.Element => {
             <Sidebar />
             <Sidebar />
           </div>
           </div>
 
 
-          <div className="flex-fill mw-0" style={{ position: 'relative' }}>
+          <div className="flex-fill mw-0">
             <AlertSiteUrlUndefined />
             <AlertSiteUrlUndefined />
             {children}
             {children}
           </div>
           </div>

+ 1 - 1
packages/app/src/components/Layout/MainPane.tsx

@@ -24,7 +24,7 @@ export const MainPane = (props: Props): JSX.Element => {
                   <div className="flex-grow-1 flex-basis-0 mw-0">
                   <div className="flex-grow-1 flex-basis-0 mw-0">
                     {children}
                     {children}
                   </div>
                   </div>
-                  <div className="grw-side-contents-container d-edit-none">
+                  <div className="grw-side-contents-container d-edit-none" data-vrt-blackout-side-contents>
                     <div className="grw-side-contents-sticky-container">
                     <div className="grw-side-contents-sticky-container">
                       {sideContents}
                       {sideContents}
                     </div>
                     </div>

+ 1 - 1
packages/app/src/components/Layout/ShareLinkLayout.tsx

@@ -28,7 +28,7 @@ export const ShareLinkLayout = ({ children }: Props): JSX.Element => {
       <GrowiNavbar isGlobalSearchHidden={true} />
       <GrowiNavbar isGlobalSearchHidden={true} />
 
 
       <div className="page-wrapper d-flex d-print-block">
       <div className="page-wrapper d-flex d-print-block">
-        <div className="flex-fill mw-0" style={{ position: 'relative' }}>
+        <div className="flex-fill mw-0">
           {children}
           {children}
         </div>
         </div>
       </div>
       </div>

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

@@ -39,7 +39,7 @@ const ApiSettings = React.memo((): JSX.Element => {
             ? (
             ? (
               <input
               <input
                 data-testid="grw-api-settings-input"
                 data-testid="grw-api-settings-input"
-                data-hide-in-vrt
+                data-vrt-blackout
                 className="form-control"
                 className="form-control"
                 type="text"
                 type="text"
                 name="apiToken"
                 name="apiToken"

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

@@ -105,14 +105,14 @@ const ProfileImageSettings = (): JSX.Element => {
                 onChange={() => setGravatarEnabled(true)}
                 onChange={() => setGravatarEnabled(true)}
               />
               />
               <label className="custom-control-label" htmlFor="radioGravatar">
               <label className="custom-control-label" htmlFor="radioGravatar">
-                <img src={GRAVATAR_DEFAULT} data-hide-in-vrt /> Gravatar
+                <img src={GRAVATAR_DEFAULT} data-vrt-blackout-profile /> Gravatar
               </label>
               </label>
               <a href="https://gravatar.com/">
               <a href="https://gravatar.com/">
                 <small><i className="icon-arrow-right-circle" aria-hidden="true"></i></small>
                 <small><i className="icon-arrow-right-circle" aria-hidden="true"></i></small>
               </a>
               </a>
             </div>
             </div>
           </h4>
           </h4>
-          <img src={generateGravatarSrc(currentUser.email)} width="64" data-hide-in-vrt />
+          <img src={generateGravatarSrc(currentUser.email)} width="64" data-vrt-blackout-profile />
         </div>
         </div>
 
 
         <div className="col-md-6 col-12">
         <div className="col-md-6 col-12">

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

@@ -66,7 +66,7 @@ export const AuthorInfo = (props: AuthorInfoProps): JSX.Element => {
       </div>
       </div>
       <div>
       <div>
         <div>{infoLabelForSubNav} {userLabel}</div>
         <div>{infoLabelForSubNav} {userLabel}</div>
-        <div className="text-muted text-date" data-hide-in-vrt>
+        <div className="text-muted text-date" data-vrt-blackout-datetime>
           {renderParsedDate()}
           {renderParsedDate()}
         </div>
         </div>
       </div>
       </div>

+ 16 - 9
packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -1,6 +1,8 @@
 import React, { useState, useEffect, useCallback } from 'react';
 import React, { useState, useEffect, useCallback } from 'react';
 
 
-import { isPopulated, IUser, pagePathUtils } from '@growi/core';
+import {
+  isPopulated, IUser, pagePathUtils, IPagePopulatedToShowRevision,
+} from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
@@ -25,7 +27,7 @@ import {
 import { useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
 import { useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
 import {
 import {
   EditorMode, useDrawerMode, useEditorMode, useIsAbleToShowPageManagement, useIsAbleToShowTagLabel,
   EditorMode, useDrawerMode, useEditorMode, useIsAbleToShowPageManagement, useIsAbleToShowTagLabel,
-  useIsAbleToShowPageEditorModeManager, useIsAbleToShowPageAuthors,
+  useIsAbleToChangeEditorMode, useIsAbleToShowPageAuthors,
 } from '~/stores/ui';
 } from '~/stores/ui';
 
 
 import CreateTemplateModal from '../CreateTemplateModal';
 import CreateTemplateModal from '../CreateTemplateModal';
@@ -182,16 +184,19 @@ const CreateTemplateMenuItems = (props: CreateTemplateMenuItemsProps): JSX.Eleme
 };
 };
 
 
 type GrowiContextualSubNavigationProps = {
 type GrowiContextualSubNavigationProps = {
+  currentPage?: IPagePopulatedToShowRevision,
   isCompactMode?: boolean,
   isCompactMode?: boolean,
   isLinkSharingDisabled: boolean,
   isLinkSharingDisabled: boolean,
 };
 };
 
 
 const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps): JSX.Element => {
 const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps): JSX.Element => {
 
 
+  const { currentPage } = props;
+
   const router = useRouter();
   const router = useRouter();
 
 
   const { data: shareLinkId } = useShareLinkId();
   const { data: shareLinkId } = useShareLinkId();
-  const { data: currentPage, mutate: mutateCurrentPage } = useSWRxCurrentPage(shareLinkId ?? undefined);
+  const { mutate: mutateCurrentPage } = useSWRxCurrentPage();
 
 
   const { data: currentPathname } = useCurrentPathname();
   const { data: currentPathname } = useCurrentPathname();
   const isSharedPage = pagePathUtils.isSharedPage(currentPathname ?? '');
   const isSharedPage = pagePathUtils.isSharedPage(currentPathname ?? '');
@@ -211,10 +216,10 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
 
 
   const { data: isAbleToShowPageManagement } = useIsAbleToShowPageManagement();
   const { data: isAbleToShowPageManagement } = useIsAbleToShowPageManagement();
   const { data: isAbleToShowTagLabel } = useIsAbleToShowTagLabel();
   const { data: isAbleToShowTagLabel } = useIsAbleToShowTagLabel();
-  const { data: isAbleToShowPageEditorModeManager } = useIsAbleToShowPageEditorModeManager();
+  const { data: isAbleToChangeEditorMode } = useIsAbleToChangeEditorMode();
   const { data: isAbleToShowPageAuthors } = useIsAbleToShowPageAuthors();
   const { data: isAbleToShowPageAuthors } = useIsAbleToShowPageAuthors();
 
 
-  const { mutate: mutateSWRTagsInfo, data: tagsInfoData } = useSWRxTagsInfo(!isSharedPage ? currentPage?._id : undefined);
+  const { mutate: mutateSWRTagsInfo, data: tagsInfoData } = useSWRxTagsInfo(currentPage?._id);
 
 
   // eslint-disable-next-line max-len
   // eslint-disable-next-line max-len
   const { data: tagsForEditors, mutate: mutatePageTagsForEditors, sync: syncPageTagsForEditors } = usePageTagsForEditors(!isSharedPage ? currentPage?._id : undefined);
   const { data: tagsForEditors, mutate: mutatePageTagsForEditors, sync: syncPageTagsForEditors } = usePageTagsForEditors(!isSharedPage ? currentPage?._id : undefined);
@@ -314,9 +319,11 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
   }, [currentPathname, openDeleteModal, router]);
   }, [currentPathname, openDeleteModal, router]);
 
 
   const switchContentWidthHandler = useCallback(async(pageId: string, value: boolean) => {
   const switchContentWidthHandler = useCallback(async(pageId: string, value: boolean) => {
-    await updateContentWidth(pageId, value);
-    mutateCurrentPage();
-  }, [mutateCurrentPage]);
+    if (!isSharedPage) {
+      await updateContentWidth(pageId, value);
+      mutateCurrentPage();
+    }
+  }, [isSharedPage, mutateCurrentPage]);
 
 
   const templateMenuItemClickHandler = useCallback(() => {
   const templateMenuItemClickHandler = useCallback(() => {
     setIsPageTempleteModalShown(true);
     setIsPageTempleteModalShown(true);
@@ -373,7 +380,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
                 ) }
                 ) }
               </div>
               </div>
             ) }
             ) }
-            {isAbleToShowPageEditorModeManager && (
+            {isAbleToChangeEditorMode && (
               <PageEditorModeManager
               <PageEditorModeManager
                 onPageEditorModeButtonClicked={viewType => mutateEditorMode(viewType)}
                 onPageEditorModeButtonClicked={viewType => mutateEditorMode(viewType)}
                 isBtnDisabled={isGuestUser}
                 isBtnDisabled={isGuestUser}

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

@@ -11,7 +11,7 @@ import { useRipple } from 'react-use-ripple';
 import { UncontrolledTooltip } from 'reactstrap';
 import { UncontrolledTooltip } from 'reactstrap';
 
 
 import {
 import {
-  useIsSearchPage, useIsGuestUser, useIsSearchServiceConfigured, useAppTitle, useConfidential, useCustomizedLogoSrc,
+  useIsSearchPage, useIsGuestUser, useIsSearchServiceConfigured, useAppTitle, useConfidential, useIsDefaultLogo,
 } from '~/stores/context';
 } from '~/stores/context';
 import { usePageCreateModal } from '~/stores/modal';
 import { usePageCreateModal } from '~/stores/modal';
 import { useCurrentPagePath } from '~/stores/page';
 import { useCurrentPagePath } from '~/stores/page';
@@ -122,16 +122,16 @@ const Confidential: FC<ConfidentialProps> = memo((props: ConfidentialProps): JSX
 Confidential.displayName = 'Confidential';
 Confidential.displayName = 'Confidential';
 
 
 interface NavbarLogoProps {
 interface NavbarLogoProps {
-  logoSrc?: string,
+  isDefaultLogo?: boolean
 }
 }
 
 
 const GrowiNavbarLogo: FC<NavbarLogoProps> = memo((props: NavbarLogoProps) => {
 const GrowiNavbarLogo: FC<NavbarLogoProps> = memo((props: NavbarLogoProps) => {
-  const { logoSrc } = props;
+  const { isDefaultLogo } = props;
 
 
-  return logoSrc != null
+  return isDefaultLogo
+    ? <GrowiLogo />
     // eslint-disable-next-line @next/next/no-img-element
     // eslint-disable-next-line @next/next/no-img-element
-    ? (<img src={logoSrc} alt="custom logo" className="picture picture-lg p-2 mx-2" id="settingBrandLogo" width="32" />)
-    : <GrowiLogo />;
+    : (<img src='/attachment/brand-logo' alt="custom logo" className="picture picture-lg p-2 mx-2" id="settingBrandLogo" width="32" />);
 });
 });
 
 
 GrowiNavbarLogo.displayName = 'GrowiNavbarLogo';
 GrowiNavbarLogo.displayName = 'GrowiNavbarLogo';
@@ -151,7 +151,7 @@ export const GrowiNavbar = (props: Props): JSX.Element => {
   const { data: isSearchServiceConfigured } = useIsSearchServiceConfigured();
   const { data: isSearchServiceConfigured } = useIsSearchServiceConfigured();
   const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
   const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
   const { data: isSearchPage } = useIsSearchPage();
   const { data: isSearchPage } = useIsSearchPage();
-  const { data: customizedLogoSrc } = useCustomizedLogoSrc();
+  const { data: isDefaultLogo } = useIsDefaultLogo();
 
 
   return (
   return (
     <nav id="grw-navbar" className={`navbar grw-navbar ${styles['grw-navbar']} navbar-expand navbar-dark sticky-top mb-0 px-0`}>
     <nav id="grw-navbar" className={`navbar grw-navbar ${styles['grw-navbar']} navbar-expand navbar-dark sticky-top mb-0 px-0`}>
@@ -159,7 +159,7 @@ export const GrowiNavbar = (props: Props): JSX.Element => {
       <div className="navbar-brand mr-0">
       <div className="navbar-brand mr-0">
         <Link href="/" prefetch={false}>
         <Link href="/" prefetch={false}>
           <a className="grw-logo d-block">
           <a className="grw-logo d-block">
-            <GrowiNavbarLogo logoSrc={customizedLogoSrc}/>
+            <GrowiNavbarLogo isDefaultLogo={isDefaultLogo} />
           </a>
           </a>
         </Link>
         </Link>
       </div>
       </div>

+ 0 - 9
packages/app/src/components/Navbar/GrowiSubNavigation.module.scss

@@ -172,12 +172,3 @@
     }
     }
   }
   }
 }
 }
-
-/*
- * shadow
- */
-.grw-subnav-append-shadow-container {
-  .grw-subnav {
-    box-shadow: 0px 0px 6px 3px rgba(black, 0.15);
-  }
-}

+ 0 - 138
packages/app/src/components/Navbar/GrowiSubNavigationSwitcher.jsx

@@ -1,138 +0,0 @@
-import React, {
-  useMemo, useState, useRef, useEffect, useCallback,
-} from 'react';
-
-import PropTypes from 'prop-types';
-import StickyEvents from 'sticky-events';
-import { debounce } from 'throttle-debounce';
-
-import { useSidebarCollapsed } from '~/stores/ui';
-import loggerFactory from '~/utils/logger';
-
-import GrowiContextualSubNavigation from './GrowiContextualSubNavigation';
-
-import styles from './GrowiSubNavigationSwitcher.module.scss';
-
-const logger = loggerFactory('growi:cli:GrowiSubNavigationSticky');
-
-
-/**
- * Subnavigation
- *
- * needs:
- *   #grw-subnav-fixed-container element
- *   #grw-subnav-sticky-trigger element
- *
- * @param {object} props
- */
-const GrowiSubNavigationSwitcher = (props) => {
-
-  const { data: isSidebarCollapsed } = useSidebarCollapsed();
-
-  const [isVisible, setVisible] = useState(false);
-  const [width, setWidth] = useState(null);
-
-  const fixedContainerRef = useRef();
-  /*
-  * Comment out to prevent err >>> TypeError: Cannot read properties of null (reading 'bottom')
-  * The above err occurs when moving to admin page after rendering normal pages.
-  * This is because id "grw-subnav-sticky-trigger" does not exist on admin pages.
-  */
-  // const stickyEvents = useMemo(() => new StickyEvents({ stickySelector: '#grw-subnav-sticky-trigger' }), []);
-
-  const initWidth = useCallback(() => {
-    const instance = fixedContainerRef.current;
-
-    if (instance == null || instance.parentNode == null) {
-      return;
-    }
-
-    // get parent width
-    const { clientWidth } = instance.parentNode;
-    // update style
-    setWidth(clientWidth);
-  }, []);
-
-  // const initVisible = useCallback(() => {
-  //   const elements = stickyEvents.stickyElements;
-
-  //   for (const elem of elements) {
-  //     const bool = stickyEvents.isSticking(elem);
-  //     if (bool) {
-  //       setVisible(bool);
-  //       break;
-  //     }
-  //   }
-
-  // }, [stickyEvents]);
-
-  // setup effect by resizing event
-  useEffect(() => {
-    const resizeHandler = debounce(100, initWidth);
-
-    window.addEventListener('resize', resizeHandler);
-
-    // return clean up handler
-    return () => {
-      window.removeEventListener('resize', resizeHandler);
-    };
-  }, [initWidth]);
-
-  const stickyChangeHandler = useCallback((event) => {
-    logger.debug('StickyEvents.CHANGE detected');
-    setVisible(event.detail.isSticky);
-  }, []);
-
-  // // setup effect by sticky event
-  // useEffect(() => {
-  //   // sticky
-  //   // See: https://github.com/ryanwalters/sticky-events
-  //   const { stickySelector } = stickyEvents;
-  //   const elem = document.querySelector(stickySelector);
-  //   elem.addEventListener(StickyEvents.CHANGE, stickyChangeHandler);
-
-  //   // return clean up handler
-  //   return () => {
-  //     elem.removeEventListener(StickyEvents.CHANGE, stickyChangeHandler);
-  //   };
-  // }, [stickyChangeHandler, stickyEvents]);
-
-  // update width when sidebar collapsing changed
-  useEffect(() => {
-    if (isSidebarCollapsed != null) {
-      setTimeout(initWidth, 300);
-    }
-  }, [isSidebarCollapsed, initWidth]);
-
-  // // initialize
-  // useEffect(() => {
-  //   initWidth();
-
-  //   // check sticky state several times
-  //   setTimeout(initVisible, 100);
-  //   setTimeout(initVisible, 300);
-  //   setTimeout(initVisible, 2000);
-
-  // }, [initWidth, initVisible]);
-
-  // ${styles['grw-subnav-switcher']}
-
-  return (
-    <div className={`${styles['grw-subnav-switcher']} ${isVisible ? '' : 'grw-subnav-switcher-hidden'}`}>
-      <div
-        id="grw-subnav-fixed-container"
-        className={`grw-subnav-fixed-container ${styles['grw-subnav-fixed-container']} position-fixed grw-subnav-append-shadow-container`}
-        ref={fixedContainerRef}
-        style={{ width }}
-      >
-        <GrowiContextualSubNavigation isCompactMode isLinkSharingDisabled />
-      </div>
-    </div>
-  );
-};
-
-GrowiSubNavigationSwitcher.propTypes = {
-  isLinkSharingDisabled: PropTypes.bool,
-};
-
-export default GrowiSubNavigationSwitcher;

+ 9 - 0
packages/app/src/components/Navbar/GrowiSubNavigationSwitcher.module.scss

@@ -19,6 +19,15 @@ $easeInOutCubic: cubic-bezier(0.65, 0, 0.35, 1);
     .grw-subnav-fixed-container {
     .grw-subnav-fixed-container {
       transition: transform 150ms $easeInOutCubic;
       transition: transform 150ms $easeInOutCubic;
     }
     }
+
+    /*
+    * shadow
+    */
+    .grw-subnav-append-shadow-container {
+      .grw-subnav {
+        box-shadow: 0px 0px 6px 3px rgba(black, 0.15);
+      }
+    }
   }
   }
 
 
   &:global {
   &:global {

+ 103 - 0
packages/app/src/components/Navbar/GrowiSubNavigationSwitcher.tsx

@@ -0,0 +1,103 @@
+import React, {
+  useState, useRef, useEffect, useCallback,
+} from 'react';
+
+import StickyEvents from 'sticky-events';
+import { debounce } from 'throttle-debounce';
+
+import { useSWRxCurrentPage } from '~/stores/page';
+import { useSidebarCollapsed } from '~/stores/ui';
+import loggerFactory from '~/utils/logger';
+
+import GrowiContextualSubNavigation from './GrowiContextualSubNavigation';
+
+import styles from './GrowiSubNavigationSwitcher.module.scss';
+
+const logger = loggerFactory('growi:cli:GrowiSubNavigationSticky');
+
+/**
+ * GrowiSubNavigation
+ *
+ * needs:
+ *   #grw-subnav-fixed-container element
+ *   #grw-subnav-sticky-trigger element
+ */
+export const GrowiSubNavigationSwitcher = (): JSX.Element => {
+
+  const { data: currentPage } = useSWRxCurrentPage();
+  const { data: isSidebarCollapsed } = useSidebarCollapsed();
+
+  const [isVisible, setIsVisible] = useState<boolean>(false);
+  const [width, setWidth] = useState<number>(0);
+
+  // use more specific type HTMLDivElement for avoid assertion error.
+  // see: https://developer.mozilla.org/en-US/docs/Web/API/HTMLDivElement
+  const fixedContainerRef = useRef<HTMLDivElement>(null);
+
+  const initWidth = useCallback(() => {
+    if (fixedContainerRef.current && fixedContainerRef.current.parentElement) {
+      // get parent elements width
+      const { clientWidth } = fixedContainerRef.current.parentElement;
+      setWidth(clientWidth);
+    }
+  }, []);
+
+  const stickyChangeHandler = useCallback((event) => {
+    logger.debug('StickyEvents.CHANGE detected');
+    setIsVisible(event.detail.isSticky);
+  }, []);
+
+  // setup effect by sticky-events
+  useEffect(() => {
+    // sticky-events
+    // See: https://github.com/ryanwalters/sticky-events
+    const { stickySelector } = new StickyEvents({ stickySelector: '#grw-subnav-sticky-trigger' });
+    const elem = document.querySelector(stickySelector);
+    elem.addEventListener(StickyEvents.CHANGE, stickyChangeHandler);
+
+    // return clean up handler
+    return () => {
+      elem.removeEventListener(StickyEvents.CHANGE, stickyChangeHandler);
+    };
+  }, [stickyChangeHandler]);
+
+  // setup effect by resizing event
+  useEffect(() => {
+    const resizeHandler = debounce(100, initWidth);
+    window.addEventListener('resize', resizeHandler);
+
+    // return clean up handler
+    return () => {
+      window.removeEventListener('resize', resizeHandler);
+    };
+  }, [initWidth]);
+
+  // update width when sidebar collapsing changed
+  useEffect(() => {
+    if (isSidebarCollapsed != null) {
+      setTimeout(initWidth, 300);
+    }
+  }, [isSidebarCollapsed, initWidth]);
+
+  // initialize width
+  useEffect(() => {
+    initWidth();
+  }, [initWidth]);
+
+  if (currentPage == null) {
+    return <></>;
+  }
+
+  return (
+    <div className={`${styles['grw-subnav-switcher']} ${isVisible ? '' : 'grw-subnav-switcher-hidden'}`}>
+      <div
+        id="grw-subnav-fixed-container"
+        className={`grw-subnav-fixed-container ${styles['grw-subnav-fixed-container']} position-fixed grw-subnav-append-shadow-container`}
+        ref={fixedContainerRef}
+        style={{ width }}
+      >
+        <GrowiContextualSubNavigation currentPage={currentPage} isCompactMode isLinkSharingDisabled />
+      </div>
+    </div>
+  );
+};

+ 17 - 9
packages/app/src/components/Page.tsx

@@ -1,11 +1,11 @@
 import React, {
 import React, {
-  useCallback,
+  FC, useCallback,
   useEffect, useRef,
   useEffect, useRef,
 } from 'react';
 } from 'react';
 
 
 import EventEmitter from 'events';
 import EventEmitter from 'events';
 
 
-import { pagePathUtils } from '@growi/core';
+import { pagePathUtils, IPagePopulatedToShowRevision } from '@growi/core';
 import { DrawioEditByViewerProps } from '@growi/remark-drawio';
 import { DrawioEditByViewerProps } from '@growi/remark-drawio';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
@@ -48,9 +48,13 @@ const LinkEditModal = dynamic(() => import('./PageEditor/LinkEditModal'), { ssr:
 
 
 const logger = loggerFactory('growi:Page');
 const logger = loggerFactory('growi:Page');
 
 
+type Props = {
+  currentPage?: IPagePopulatedToShowRevision,
+}
 
 
-export const Page = (props) => {
+export const Page: FC<Props> = (props: Props) => {
   const { t } = useTranslation();
   const { t } = useTranslation();
+  const { currentPage } = props;
 
 
   // Pass tocRef to generateViewOptions (=> rehypePlugin => customizeTOC) to call mutateCurrentPageTocNode when tocRef.current changes.
   // Pass tocRef to generateViewOptions (=> rehypePlugin => customizeTOC) to call mutateCurrentPageTocNode when tocRef.current changes.
   // The toc node passed by customizeTOC is assigned to tocRef.current.
   // The toc node passed by customizeTOC is assigned to tocRef.current.
@@ -64,9 +68,9 @@ export const Page = (props) => {
   const isSharedPage = pagePathUtils.isSharedPage(currentPathname ?? '');
   const isSharedPage = pagePathUtils.isSharedPage(currentPathname ?? '');
 
 
   const { data: shareLinkId } = useShareLinkId();
   const { data: shareLinkId } = useShareLinkId();
-  const { data: currentPage, mutate: mutateCurrentPage } = useSWRxCurrentPage(shareLinkId ?? undefined);
+  const { mutate: mutateCurrentPage } = useSWRxCurrentPage();
   const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
   const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
-  const { data: tagsInfo } = useSWRxTagsInfo(!isSharedPage ? currentPage?._id : undefined);
+  const { data: tagsInfo } = useSWRxTagsInfo(currentPage?._id);
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isMobile } = useIsMobile();
   const { data: isMobile } = useIsMobile();
   const { data: rendererOptions, mutate: mutateRendererOptions } = useViewOptions(storeTocNodeHandler);
   const { data: rendererOptions, mutate: mutateRendererOptions } = useViewOptions(storeTocNodeHandler);
@@ -128,14 +132,16 @@ export const Page = (props) => {
       toastSuccess(t('toaster.save_succeeded'));
       toastSuccess(t('toaster.save_succeeded'));
 
 
       // rerender
       // rerender
-      mutateCurrentPage();
+      if (!isSharedPage) {
+        mutateCurrentPage();
+      }
       mutateEditingMarkdown(newMarkdown);
       mutateEditingMarkdown(newMarkdown);
     }
     }
     catch (error) {
     catch (error) {
       logger.error('failed to save', error);
       logger.error('failed to save', error);
       toastError(error);
       toastError(error);
     }
     }
-  }, [currentPage, mutateCurrentPage, mutateEditingMarkdown, saveOrUpdate, shareLinkId, t, tagsInfo]);
+  }, [currentPage, isSharedPage, mutateCurrentPage, mutateEditingMarkdown, saveOrUpdate, shareLinkId, t, tagsInfo]);
 
 
   // set handler to open DrawioModal
   // set handler to open DrawioModal
   useEffect(() => {
   useEffect(() => {
@@ -182,14 +188,16 @@ export const Page = (props) => {
       toastSuccess(t('toaster.save_succeeded'));
       toastSuccess(t('toaster.save_succeeded'));
 
 
       // rerender
       // rerender
-      mutateCurrentPage();
+      if (!isSharedPage) {
+        mutateCurrentPage();
+      }
       mutateEditingMarkdown(newMarkdown);
       mutateEditingMarkdown(newMarkdown);
     }
     }
     catch (error) {
     catch (error) {
       logger.error('failed to save', error);
       logger.error('failed to save', error);
       toastError(error);
       toastError(error);
     }
     }
-  }, [currentPage, mutateCurrentPage, mutateEditingMarkdown, saveOrUpdate, shareLinkId, t, tagsInfo]);
+  }, [currentPage, isSharedPage, mutateCurrentPage, mutateEditingMarkdown, saveOrUpdate, shareLinkId, t, tagsInfo]);
 
 
   // set handler to open HandsonTableModal
   // set handler to open HandsonTableModal
   useEffect(() => {
   useEffect(() => {

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

@@ -34,7 +34,7 @@ const PageView = React.memo((): JSX.Element => {
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: shareLinkId } = useShareLinkId();
   const { data: shareLinkId } = useShareLinkId();
   const { data: isNotFound } = useIsNotFound();
   const { data: isNotFound } = useIsNotFound();
-  const { data: currentPage } = useSWRxCurrentPage(shareLinkId ?? undefined);
+  const { data: currentPage } = useSWRxCurrentPage();
   const { setRemoteLatestPageData } = useSetRemoteLatestPageData();
   const { setRemoteLatestPageData } = useSetRemoteLatestPageData();
 
 
   const { mutate: mutateIsHackmdDraftUpdatingInRealtime } = useIsHackmdDraftUpdatingInRealtime();
   const { mutate: mutateIsHackmdDraftUpdatingInRealtime } = useIsHackmdDraftUpdatingInRealtime();
@@ -92,7 +92,7 @@ const PageView = React.memo((): JSX.Element => {
   return (
   return (
     <>
     <>
       { isUsersHomePagePath && <UserInfo author={currentPage?.creator} /> }
       { isUsersHomePagePath && <UserInfo author={currentPage?.creator} /> }
-      { !isNotFound && <Page /> }
+      { !isNotFound && <Page currentPage={currentPage ?? undefined} /> }
       { isNotFound && <NotFoundPage /> }
       { isNotFound && <NotFoundPage /> }
     </>
     </>
   );
   );

+ 5 - 3
packages/app/src/components/Page/TagEditModal.jsx

@@ -1,6 +1,7 @@
 import React, { useState, useEffect } from 'react';
 import React, { useState, useEffect } from 'react';
-import PropTypes from 'prop-types';
 
 
+import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
 import {
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 } from 'reactstrap';
@@ -9,6 +10,7 @@ import TagsInput from './TagsInput';
 
 
 function TagEditModal(props) {
 function TagEditModal(props) {
   const [tags, setTags] = useState([]);
   const [tags, setTags] = useState([]);
+  const { t } = useTranslation();
 
 
   function onTagsUpdatedByTagsInput(tags) {
   function onTagsUpdatedByTagsInput(tags) {
     setTags(tags);
     setTags(tags);
@@ -37,14 +39,14 @@ function TagEditModal(props) {
   return (
   return (
     <Modal isOpen={props.isOpen} toggle={closeModalHandler} id="edit-tag-modal" autoFocus={false}>
     <Modal isOpen={props.isOpen} toggle={closeModalHandler} id="edit-tag-modal" autoFocus={false}>
       <ModalHeader tag="h4" toggle={closeModalHandler} className="bg-primary text-light">
       <ModalHeader tag="h4" toggle={closeModalHandler} className="bg-primary text-light">
-        Edit Tags
+        {t('tag_edit_modal.edit_tags')}
       </ModalHeader>
       </ModalHeader>
       <ModalBody>
       <ModalBody>
         <TagsInput tags={tags} onTagsUpdated={onTagsUpdatedByTagsInput} autoFocus />
         <TagsInput tags={tags} onTagsUpdated={onTagsUpdatedByTagsInput} autoFocus />
       </ModalBody>
       </ModalBody>
       <ModalFooter>
       <ModalFooter>
         <button type="button" className="btn btn-primary" onClick={handleSubmit}>
         <button type="button" className="btn btn-primary" onClick={handleSubmit}>
-          Done
+          {t('tag_edit_modal.done')}
         </button>
         </button>
       </ModalFooter>
       </ModalFooter>
     </Modal>
     </Modal>

+ 3 - 1
packages/app/src/components/Page/TagsInput.tsx

@@ -2,6 +2,7 @@ import React, {
   FC, useRef, useState, useCallback,
   FC, useRef, useState, useCallback,
 } from 'react';
 } from 'react';
 
 
+import { useTranslation } from 'next-i18next';
 import { AsyncTypeahead } from 'react-bootstrap-typeahead';
 import { AsyncTypeahead } from 'react-bootstrap-typeahead';
 
 
 import { useSWRxTagsSearch } from '~/stores/tag';
 import { useSWRxTagsSearch } from '~/stores/tag';
@@ -20,6 +21,7 @@ type Props = {
 }
 }
 
 
 const TagsInput: FC<Props> = (props: Props) => {
 const TagsInput: FC<Props> = (props: Props) => {
+  const { t } = useTranslation();
   const tagsInputRef = useRef<TypeaheadInstance>(null);
   const tagsInputRef = useRef<TypeaheadInstance>(null);
 
 
   const [resultTags, setResultTags] = useState<string[]>([]);
   const [resultTags, setResultTags] = useState<string[]>([]);
@@ -71,7 +73,7 @@ const TagsInput: FC<Props> = (props: Props) => {
         onSearch={searchHandler}
         onSearch={searchHandler}
         onKeyDown={keyDownHandler}
         onKeyDown={keyDownHandler}
         options={resultTags} // Search result (Some tag names)
         options={resultTags} // Search result (Some tag names)
-        placeholder="tag name"
+        placeholder={t('tag_edit_modal.tags_input.tag_name')}
         autoFocus={props.autoFocus}
         autoFocus={props.autoFocus}
       />
       />
     </div>
     </div>

+ 1 - 1
packages/app/src/components/PageAlert/TrashPageAlert.tsx

@@ -114,7 +114,7 @@ export const TrashPageAlert = (): JSX.Element => {
           <br />
           <br />
           <UserPicture user={deleteUser} />
           <UserPicture user={deleteUser} />
           <span className="ml-2">
           <span className="ml-2">
-            Deleted by { deleteUser?.name } at {deletedAt || pageData?.updatedAt}
+            Deleted by { deleteUser?.name } at <span data-vrt-blackout-datetime>{deletedAt || pageData?.updatedAt}</span>
           </span>
           </span>
         </div>
         </div>
         <div className="pt-1 d-flex align-items-end align-items-lg-center">
         <div className="pt-1 d-flex align-items-end align-items-lg-center">

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

@@ -2,8 +2,9 @@ import React, {
   useCallback, useEffect, useMemo, useRef, useState,
   useCallback, useEffect, useMemo, useRef, useState,
 } from 'react';
 } from 'react';
 
 
-
 import EventEmitter from 'events';
 import EventEmitter from 'events';
+import nodePath from 'path';
+
 
 
 import {
 import {
   IPageHasId, PageGrant, pathUtils,
   IPageHasId, PageGrant, pathUtils,
@@ -31,6 +32,7 @@ import {
 } from '~/stores/editor';
 } from '~/stores/editor';
 import { useConflictDiffModal } from '~/stores/modal';
 import { useConflictDiffModal } from '~/stores/modal';
 import { useCurrentPagePath, useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
 import { useCurrentPagePath, useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
+import { usePageTreeTermManager } from '~/stores/page-listing';
 import { useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
 import { useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
 import { usePreviewOptions } from '~/stores/renderer';
 import { usePreviewOptions } from '~/stores/renderer';
 import {
 import {
@@ -88,6 +90,7 @@ const PageEditor = React.memo((): JSX.Element => {
   const { data: isUploadableFile } = useIsUploadableFile();
   const { data: isUploadableFile } = useIsUploadableFile();
   const { data: isUploadableImage } = useIsUploadableImage();
   const { data: isUploadableImage } = useIsUploadableImage();
   const { data: conflictDiffModalStatus, close: closeConflictDiffModal } = useConflictDiffModal();
   const { data: conflictDiffModalStatus, close: closeConflictDiffModal } = useConflictDiffModal();
+  const { advance: advancePt } = usePageTreeTermManager();
 
 
   const { data: rendererOptions, mutate: mutateRendererOptions } = usePreviewOptions();
   const { data: rendererOptions, mutate: mutateRendererOptions } = usePreviewOptions();
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
@@ -104,7 +107,8 @@ const PageEditor = React.memo((): JSX.Element => {
 
 
     let initialValue = '';
     let initialValue = '';
     if (isEnabledAttachTitleHeader && currentPathname != null) {
     if (isEnabledAttachTitleHeader && currentPathname != null) {
-      initialValue += `${pathUtils.attachTitleHeader(currentPathname)}\n`;
+      const pageTitle = nodePath.basename(currentPathname);
+      initialValue += `${pathUtils.attachTitleHeader(pageTitle)}\n`;
     }
     }
     if (templateBodyData != null) {
     if (templateBodyData != null) {
       initialValue += `${templateBodyData}\n`;
       initialValue += `${templateBodyData}\n`;
@@ -204,6 +208,9 @@ const PageEditor = React.memo((): JSX.Element => {
         options,
         options,
       );
       );
 
 
+      // to sync revision id with page tree: https://github.com/weseek/growi/pull/7227
+      advancePt();
+
       return page;
       return page;
     }
     }
     catch (error) {
     catch (error) {
@@ -221,7 +228,7 @@ const PageEditor = React.memo((): JSX.Element => {
     }
     }
 
 
   // eslint-disable-next-line max-len
   // eslint-disable-next-line max-len
-  }, [currentPathname, optionsToSave, grantData, isSlackEnabled, saveOrUpdate, pageId, currentPagePath, currentRevisionId]);
+  }, [currentPathname, optionsToSave, grantData, isSlackEnabled, saveOrUpdate, pageId, currentPagePath, currentRevisionId, advancePt]);
 
 
   const saveAndReturnToViewHandler = useCallback(async(opts: {slackChannels: string, overwriteScopesOfDescendants?: boolean}) => {
   const saveAndReturnToViewHandler = useCallback(async(opts: {slackChannels: string, overwriteScopesOfDescendants?: boolean}) => {
     if (editorMode !== EditorMode.Editor) {
     if (editorMode !== EditorMode.Editor) {

+ 6 - 1
packages/app/src/components/PageEditor/DrawioModal.tsx

@@ -37,7 +37,12 @@ const drawioConfig = {
 
 
 export const DrawioModal = (): JSX.Element => {
 export const DrawioModal = (): JSX.Element => {
   const { data: drawioUri } = useDrawioUri();
   const { data: drawioUri } = useDrawioUri();
-  const { data: personalSettingsInfo } = usePersonalSettings();
+  const { data: personalSettingsInfo } = usePersonalSettings({
+    // make immutable
+    revalidateIfStale: false,
+    revalidateOnFocus: false,
+    revalidateOnReconnect: false,
+  });
 
 
   const { data: drawioModalData, close: closeDrawioModal } = useDrawioModal();
   const { data: drawioModalData, close: closeDrawioModal } = useDrawioModal();
   const isOpened = drawioModalData?.isOpened ?? false;
   const isOpened = drawioModalData?.isOpened ?? false;

+ 6 - 1
packages/app/src/components/PageEditor/ScrollSyncHelper.js

@@ -77,6 +77,11 @@ class ScrollSyncHelper {
     }
     }
 
 
     const hiElement = lines[hi];
     const hiElement = lines[hi];
+
+    if (hiElement == null) {
+      return {};
+    }
+
     if (hi >= 1 && hiElement.element.getBoundingClientRect().top > position) {
     if (hi >= 1 && hiElement.element.getBoundingClientRect().top > position) {
       const loElement = lines[lo];
       const loElement = lines[lo];
       const bounds = loElement.element.getBoundingClientRect();
       const bounds = loElement.element.getBoundingClientRect();
@@ -95,7 +100,7 @@ class ScrollSyncHelper {
 
 
   getEditorLineNumberForPageOffset(parentElement, offset) {
   getEditorLineNumberForPageOffset(parentElement, offset) {
     const { previous, next } = this.getLineElementsAtPageOffset(parentElement, offset);
     const { previous, next } = this.getLineElementsAtPageOffset(parentElement, offset);
-    if (previous) {
+    if (previous != null) {
       if (next) {
       if (next) {
         const betweenProgress = (
         const betweenProgress = (
           offset - parentElement.scrollTop - previous.element.getBoundingClientRect().top)
           offset - parentElement.scrollTop - previous.element.getBoundingClientRect().top)

+ 12 - 3
packages/app/src/components/PageEditorByHackmd.tsx

@@ -25,6 +25,7 @@ import {
   usePageIdOnHackmd, useHasDraftOnHackmd, useRevisionIdHackmdSynced, useIsHackmdDraftUpdatingInRealtime,
   usePageIdOnHackmd, useHasDraftOnHackmd, useRevisionIdHackmdSynced, useIsHackmdDraftUpdatingInRealtime,
 } from '~/stores/hackmd';
 } from '~/stores/hackmd';
 import { useCurrentPagePath, useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
 import { useCurrentPagePath, useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
+import { usePageTreeTermManager } from '~/stores/page-listing';
 import { useRemoteRevisionId } from '~/stores/remote-latest-page';
 import { useRemoteRevisionId } from '~/stores/remote-latest-page';
 import {
 import {
   EditorMode,
   EditorMode,
@@ -63,6 +64,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
   const { data: grantData } = useSelectedGrant();
   const { data: grantData } = useSelectedGrant();
   const { data: hackmdUri } = useHackmdUri();
   const { data: hackmdUri } = useHackmdUri();
   const saveOrUpdate = useSaveOrUpdate();
   const saveOrUpdate = useSaveOrUpdate();
+  const { advance: advancePt } = usePageTreeTermManager();
 
 
   const { returnPathForURL } = pathUtils;
   const { returnPathForURL } = pathUtils;
 
 
@@ -127,6 +129,9 @@ export const PageEditorByHackmd = (): JSX.Element => {
       else {
       else {
         updateStateAfterSave?.();
         updateStateAfterSave?.();
         mutateIsHackmdDraftUpdatingInRealtime(false);
         mutateIsHackmdDraftUpdatingInRealtime(false);
+
+        // to sync revision id with page tree: https://github.com/weseek/growi/pull/7227
+        advancePt();
       }
       }
       setIsInitialized(false);
       setIsInitialized(false);
       mutateEditorMode(EditorMode.View);
       mutateEditorMode(EditorMode.View);
@@ -136,7 +141,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, isNotFound, mutateEditorMode, router, updateStateAfterSave, mutateIsHackmdDraftUpdatingInRealtime]);
+  }, [editorMode, currentPathname, revision, revisionIdHackmdSynced, optionsToSave, saveOrUpdate, pageId, currentPagePath, isNotFound, mutateEditorMode, router, updateStateAfterSave, mutateIsHackmdDraftUpdatingInRealtime, advancePt]);
 
 
   // set handler to save and reload Page
   // set handler to save and reload Page
   useEffect(() => {
   useEffect(() => {
@@ -258,6 +263,9 @@ export const PageEditorByHackmd = (): JSX.Element => {
       updateStateAfterSave?.();
       updateStateAfterSave?.();
       mutateTagsInfo();
       mutateTagsInfo();
 
 
+      // to sync revision id with page tree: https://github.com/weseek/growi/pull/7227
+      advancePt();
+
       mutateIsEnabledUnsavedWarning(false);
       mutateIsEnabledUnsavedWarning(false);
 
 
       logger.debug('success to save');
       logger.debug('success to save');
@@ -268,8 +276,9 @@ 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, mutateIsEnabledUnsavedWarning, t]);
+  }, [
+    currentPagePath, currentPathname, pageId, revisionIdHackmdSynced, optionsToSave,
+    saveOrUpdate, mutatePageData, updateStateAfterSave, mutateTagsInfo, advancePt, mutateIsEnabledUnsavedWarning, t]);
 
 
   /**
   /**
    * onChange event of HackmdEditor handler
    * onChange event of HackmdEditor handler

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

@@ -29,7 +29,7 @@ import PaginationWrapper from './PaginationWrapper';
 import { PrivateLegacyPagesMigrationModal } from './PrivateLegacyPagesMigrationModal';
 import { PrivateLegacyPagesMigrationModal } from './PrivateLegacyPagesMigrationModal';
 import { OperateAllControl } from './SearchPage/OperateAllControl';
 import { OperateAllControl } from './SearchPage/OperateAllControl';
 import SearchControl from './SearchPage/SearchControl';
 import SearchControl from './SearchPage/SearchControl';
-import { IReturnSelectedPageIds, SearchPageBase, usePageDeleteModalForBulkDeletion } from './SearchPage2/SearchPageBase';
+import { IReturnSelectedPageIds, SearchPageBase, usePageDeleteModalForBulkDeletion } from './SearchPage/SearchPageBase';
 
 
 
 
 // TODO: replace with "customize:showPageLimitationS"
 // TODO: replace with "customize:showPageLimitationS"

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

@@ -16,7 +16,7 @@ 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';
-import { IReturnSelectedPageIds, SearchPageBase, usePageDeleteModalForBulkDeletion } from './SearchPage2/SearchPageBase';
+import { IReturnSelectedPageIds, SearchPageBase, usePageDeleteModalForBulkDeletion } from './SearchPage/SearchPageBase';
 
 
 
 
 // TODO: replace with "customize:showPageLimitationS"
 // TODO: replace with "customize:showPageLimitationS"
@@ -63,7 +63,7 @@ const SearchResultListHead = React.memo((props: SearchResultListHeadProps): JSX.
         <span className="ml-3">{`${leftNum}-${rightNum}`} / {total}</span>
         <span className="ml-3">{`${leftNum}-${rightNum}`} / {total}</span>
         { took != null && (
         { took != null && (
           // blackout 70px rectangle in VRT
           // blackout 70px rectangle in VRT
-          <span data-hide-in-vrt className="ml-3 text-muted d-inline-block" style={{ minWidth: '70px' }}>({took}ms)</span>
+          <span data-vrt-blackout className="ml-3 text-muted d-inline-block" style={{ minWidth: '70px' }}>({took}ms)</span>
         ) }
         ) }
       </div>
       </div>
       <div className="input-group flex-nowrap search-result-select-group ml-auto d-md-flex d-none">
       <div className="input-group flex-nowrap search-result-select-group ml-auto d-md-flex d-none">

+ 0 - 0
packages/app/src/components/SearchPage2/SearchPageBase.module.scss → packages/app/src/components/SearchPage/SearchPageBase.module.scss


+ 3 - 2
packages/app/src/components/SearchPage2/SearchPageBase.tsx → packages/app/src/components/SearchPage/SearchPageBase.tsx

@@ -14,7 +14,8 @@ import { usePageDeleteModal } from '~/stores/modal';
 import { usePageTreeTermManager } from '~/stores/page-listing';
 import { usePageTreeTermManager } from '~/stores/page-listing';
 
 
 import { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 import { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
-import { SearchResultList } from '../SearchPage/SearchResultList';
+
+import { SearchResultList } from './SearchResultList';
 
 
 import styles from './SearchPageBase.module.scss';
 import styles from './SearchPageBase.module.scss';
 
 
@@ -41,7 +42,7 @@ type Props = {
 }
 }
 
 
 const SearchPageBaseSubstance: ForwardRefRenderFunction<ISelectableAll & IReturnSelectedPageIds, Props> = (props:Props, ref) => {
 const SearchPageBaseSubstance: ForwardRefRenderFunction<ISelectableAll & IReturnSelectedPageIds, Props> = (props:Props, ref) => {
-  const SearchResultContent = dynamic(import('../SearchPage/SearchResultContent').then(mod => mod.SearchResultContent), { ssr: false });
+  const SearchResultContent = dynamic(import('./SearchResultContent').then(mod => mod.SearchResultContent), { ssr: false });
   const {
   const {
     pages,
     pages,
     searchingKeyword,
     searchingKeyword,

+ 8 - 0
packages/app/src/components/SearchPage/SearchResultContent.module.scss

@@ -0,0 +1,8 @@
+/*
+* shadow
+*/
+.grw-subnav-append-shadow-container :global {
+  .grw-subnav {
+    box-shadow: 0px 0px 6px 3px rgba(black, 0.15);
+  }
+}

+ 3 - 1
packages/app/src/components/SearchPage/SearchResultContent.tsx

@@ -29,6 +29,8 @@ import { ROOT_ELEM_ID as RevisionLoaderRoomElemId, RevisionLoaderProps } from '.
 import { ROOT_ELEM_ID as PageCommentRootElemId, PageCommentProps } from '../PageComment';
 import { ROOT_ELEM_ID as PageCommentRootElemId, PageCommentProps } from '../PageComment';
 import { PageContentFooterProps } from '../PageContentFooter';
 import { PageContentFooterProps } from '../PageContentFooter';
 
 
+import styles from './SearchResultContent.module.scss';
+
 
 
 const GrowiSubNavigation = dynamic<GrowiSubNavigationProps>(() => import('../Navbar/GrowiSubNavigation').then(mod => mod.GrowiSubNavigation), { ssr: false });
 const GrowiSubNavigation = dynamic<GrowiSubNavigationProps>(() => import('../Navbar/GrowiSubNavigation').then(mod => mod.GrowiSubNavigation), { ssr: false });
 const SubNavButtons = dynamic<SubNavButtonsProps>(() => import('../Navbar/SubNavButtons').then(mod => mod.SubNavButtons), { ssr: false });
 const SubNavButtons = dynamic<SubNavButtonsProps>(() => import('../Navbar/SubNavButtons').then(mod => mod.SubNavButtons), { ssr: false });
@@ -242,7 +244,7 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
 
 
   return (
   return (
     <div key={page._id} data-testid="search-result-content" className="search-result-content grw-page-path-text-muted-container d-flex flex-column">
     <div key={page._id} data-testid="search-result-content" className="search-result-content grw-page-path-text-muted-container d-flex flex-column">
-      <div className="grw-subnav-append-shadow-container">
+      <div className={`${styles['grw-subnav-append-shadow-container']} grw-subnav-append-shadow-container`}>
         <GrowiSubNavigation
         <GrowiSubNavigation
           pagePath={page.path}
           pagePath={page.path}
           pageId={page._id}
           pageId={page._id}

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

@@ -42,7 +42,7 @@ const PageItemLower = memo(({ page }: PageItemLowerProps): JSX.Element => {
         <div className="icon-bubble mr-1 d-inline-block"></div>
         <div className="icon-bubble mr-1 d-inline-block"></div>
         <div className="mr-2 grw-list-counts d-inline-block">{page.commentCount}</div>
         <div className="mr-2 grw-list-counts d-inline-block">{page.commentCount}</div>
       </div>
       </div>
-      <div className="grw-formatted-distance-date small mt-auto" data-hide-in-vrt>
+      <div className="grw-formatted-distance-date small mt-auto" data-vrt-blackout-datetime>
         <FormattedDistanceDate id={page._id} date={page.updatedAt} />
         <FormattedDistanceDate id={page._id} date={page.updatedAt} />
       </div>
       </div>
     </div>
     </div>

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

@@ -85,7 +85,7 @@ export const SidebarNav: FC<Props> = (props: Props) => {
   const { onItemSelected } = props;
   const { onItemSelected } = props;
 
 
   return (
   return (
-    <div className={`grw-sidebar-nav ${styles['grw-sidebar-nav']}`}>
+    <div className={`grw-sidebar-nav ${styles['grw-sidebar-nav']}`} data-vrt-blackout-sidebar-nav>
       <div className="grw-sidebar-nav-primary-container">
       <div className="grw-sidebar-nav-primary-container">
         {/* eslint-disable max-len */}
         {/* eslint-disable max-len */}
         <PrimaryItem contents={SidebarContentsType.TREE} label="Page Tree" iconName="format_list_bulleted" onItemSelected={onItemSelected} />
         <PrimaryItem contents={SidebarContentsType.TREE} label="Page Tree" iconName="format_list_bulleted" onItemSelected={onItemSelected} />

+ 20 - 20
packages/app/src/components/StickyStretchableScroller.tsx

@@ -69,26 +69,26 @@ export const StickyStretchableScroller: FC<StickyStretchableScrollerProps> = (pr
 
 
   const resetScrollbarDebounced = useMemo(() => debounce(100, resetScrollbar), [resetScrollbar]);
   const resetScrollbarDebounced = useMemo(() => debounce(100, resetScrollbar), [resetScrollbar]);
 
 
-  // const stickyChangeHandler = useCallback(() => {
-  //   logger.debug('StickyEvents.CHANGE detected');
-  //   resetScrollbarDebounced();
-  // }, [resetScrollbarDebounced]);
-
-  // // setup effect by sticky event
-  // useEffect(() => {
-  //   // sticky
-  //   // See: https://github.com/ryanwalters/sticky-events
-  //   const stickyEvents = new StickyEvents({ stickySelector: stickyElemSelector });
-  //   stickyEvents.enableEvents();
-  //   const { stickySelector } = stickyEvents;
-  //   const elem = document.querySelector(stickySelector);
-  //   elem.addEventListener(StickyEvents.CHANGE, stickyChangeHandler);
-
-  //   // return clean up handler
-  //   return () => {
-  //     elem.removeEventListener(StickyEvents.CHANGE, stickyChangeHandler);
-  //   };
-  // }, [stickyElemSelector, stickyChangeHandler]);
+  const stickyChangeHandler = useCallback(() => {
+    logger.debug('StickyEvents.CHANGE detected');
+    resetScrollbarDebounced();
+  }, [resetScrollbarDebounced]);
+
+  // setup effect by sticky event
+  useEffect(() => {
+    // sticky
+    // See: https://github.com/ryanwalters/sticky-events
+    const stickyEvents = new StickyEvents({ stickySelector: stickyElemSelector });
+    stickyEvents.enableEvents();
+    const { stickySelector } = stickyEvents;
+    const elem = document.querySelector(stickySelector);
+    elem.addEventListener(StickyEvents.CHANGE, stickyChangeHandler);
+
+    // return clean up handler
+    return () => {
+      elem.removeEventListener(StickyEvents.CHANGE, stickyChangeHandler);
+    };
+  }, [stickyElemSelector, stickyChangeHandler]);
 
 
   // setup effect by resizing event
   // setup effect by resizing event
   useEffect(() => {
   useEffect(() => {

+ 1 - 1
packages/app/src/components/User/UserDate.jsx

@@ -16,7 +16,7 @@ export default class UserDate extends React.Component {
     const dt = format(date, this.props.format);
     const dt = format(date, this.props.format);
 
 
     return (
     return (
-      <span className={this.props.className} data-hide-in-vrt>
+      <span className={this.props.className} data-vrt-blackout-datetime>
         {dt}
         {dt}
       </span>
       </span>
     );
     );

+ 34 - 8
packages/app/src/pages/[[...path]].page.tsx

@@ -32,6 +32,7 @@ import type { CrowiRequest } from '~/interfaces/crowi-request';
 // import { useRendererSettings } from '~/stores/renderer';
 // import { useRendererSettings } from '~/stores/renderer';
 // import { EditorMode, useEditorMode, useIsMobile } from '~/stores/ui';
 // import { EditorMode, useEditorMode, useIsMobile } from '~/stores/ui';
 import type { EditorConfig } from '~/interfaces/editor-settings';
 import type { EditorConfig } from '~/interfaces/editor-settings';
+import { IPageGrantData } from '~/interfaces/page';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 import type { IUserUISettings } from '~/interfaces/user-ui-settings';
 import type { IUserUISettings } from '~/interfaces/user-ui-settings';
@@ -71,12 +72,12 @@ import {
   useIsAclEnabled, useIsSearchPage, useTemplateTagData, useTemplateBodyData, useIsEnabledAttachTitleHeader,
   useIsAclEnabled, useIsSearchPage, useTemplateTagData, useTemplateBodyData, useIsEnabledAttachTitleHeader,
   useCsrfToken, useIsSearchScopeChildrenAsDefault, useCurrentPageId, useCurrentPathname,
   useCsrfToken, useIsSearchScopeChildrenAsDefault, useCurrentPageId, useCurrentPathname,
   useIsSlackConfigured, useRendererConfig,
   useIsSlackConfigured, useRendererConfig,
-  useEditorConfig, useIsAllReplyShown, useIsUploadableFile, useIsUploadableImage, useCustomizedLogoSrc, useIsContainerFluid, useIsNotCreatable,
+  useEditorConfig, useIsAllReplyShown, useIsUploadableFile, useIsUploadableImage, useIsContainerFluid, useIsNotCreatable,
 } from '../stores/context';
 } from '../stores/context';
 
 
 import { NextPageWithLayout } from './_app.page';
 import { NextPageWithLayout } from './_app.page';
 import {
 import {
-  CommonProps, getNextI18NextConfig, getServerSideCommonProps, generateCustomTitle,
+  CommonProps, getNextI18NextConfig, getServerSideCommonProps, generateCustomTitleForPage,
 } from './utils/commons';
 } from './utils/commons';
 
 
 
 
@@ -90,7 +91,8 @@ const NotCreatablePage = dynamic(() => import('../components/NotCreatablePage').
 const ForbiddenPage = dynamic(() => import('../components/ForbiddenPage'), { ssr: false });
 const ForbiddenPage = dynamic(() => import('../components/ForbiddenPage'), { ssr: false });
 const UnsavedAlertDialog = dynamic(() => import('../components/UnsavedAlertDialog'), { ssr: false });
 const UnsavedAlertDialog = dynamic(() => import('../components/UnsavedAlertDialog'), { ssr: false });
 const PageSideContents = dynamic<PageSideContentsProps>(() => import('../components/PageSideContents').then(mod => mod.PageSideContents), { ssr: false });
 const PageSideContents = dynamic<PageSideContentsProps>(() => import('../components/PageSideContents').then(mod => mod.PageSideContents), { ssr: false });
-const GrowiSubNavigationSwitcher = dynamic(() => import('../components/Navbar/GrowiSubNavigationSwitcher'), { ssr: false });
+const GrowiSubNavigationSwitcher = dynamic(() => import('../components/Navbar/GrowiSubNavigationSwitcher')
+  .then(mod => mod.GrowiSubNavigationSwitcher), { ssr: false });
 const UsersHomePageFooter = dynamic<UsersHomePageFooterProps>(() => import('../components/UsersHomePageFooter')
 const UsersHomePageFooter = dynamic<UsersHomePageFooterProps>(() => import('../components/UsersHomePageFooter')
   .then(mod => mod.UsersHomePageFooter), { ssr: false });
   .then(mod => mod.UsersHomePageFooter), { ssr: false });
 const DrawioModal = dynamic(() => import('../components/PageEditor/DrawioModal').then(mod => mod.DrawioModal), { ssr: false });
 const DrawioModal = dynamic(() => import('../components/PageEditor/DrawioModal').then(mod => mod.DrawioModal), { ssr: false });
@@ -184,6 +186,8 @@ type Props = CommonProps & {
   isIndentSizeForced: boolean,
   isIndentSizeForced: boolean,
   disableLinkSharing: boolean,
   disableLinkSharing: boolean,
 
 
+  grantData?: IPageGrantData,
+
   rendererConfig: RendererConfig,
   rendererConfig: RendererConfig,
 
 
   // UI
   // UI
@@ -265,7 +269,7 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   useHasDraftOnHackmd(pageWithMeta?.data.hasDraftOnHackmd ?? false);
   useHasDraftOnHackmd(pageWithMeta?.data.hasDraftOnHackmd ?? false);
   useCurrentPathname(props.currentPathname);
   useCurrentPathname(props.currentPathname);
 
 
-  useSWRxCurrentPage(undefined, pageWithMeta?.data ?? null); // store initial data
+  useSWRxCurrentPage(pageWithMeta?.data ?? null); // store initial data
 
 
   useEditingMarkdown(pageWithMeta?.data.revision?.body);
   useEditingMarkdown(pageWithMeta?.data.revision?.body);
 
 
@@ -283,8 +287,9 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
 
 
   // sync grant data
   // sync grant data
   useEffect(() => {
   useEffect(() => {
-    mutateSelectedGrant(grantData?.grantData.currentPageGrant);
-  }, [grantData?.grantData.currentPageGrant, mutateSelectedGrant]);
+    const grantDataToApply = props.grantData ? props.grantData : grantData?.grantData.currentPageGrant;
+    mutateSelectedGrant(grantDataToApply);
+  }, [grantData?.grantData.currentPageGrant, mutateSelectedGrant, props.grantData]);
 
 
   // sync pathname by Shallow Routing https://nextjs.org/docs/routing/shallow-routing
   // sync pathname by Shallow Routing https://nextjs.org/docs/routing/shallow-routing
   useEffect(() => {
   useEffect(() => {
@@ -297,7 +302,7 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
 
 
   const isTopPagePath = isTopPage(pageWithMeta?.data.path ?? '');
   const isTopPagePath = isTopPage(pageWithMeta?.data.path ?? '');
 
 
-  const title = generateCustomTitle(props, 'GROWI');
+  const title = generateCustomTitleForPage(props, pagePath ?? '');
 
 
 
 
   const sideContents = !props.isNotFound && !props.isNotCreatable
   const sideContents = !props.isNotFound && !props.isNotCreatable
@@ -328,7 +333,7 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
       <div className={`dynamic-layout-root ${growiLayoutFluidClass} h-100 d-flex flex-column justify-content-between`}>
       <div className={`dynamic-layout-root ${growiLayoutFluidClass} h-100 d-flex flex-column justify-content-between`}>
         <header className="py-0 position-relative">
         <header className="py-0 position-relative">
           <div id="grw-subnav-container">
           <div id="grw-subnav-container">
-            <GrowiContextualSubNavigation isLinkSharingDisabled={props.disableLinkSharing} />
+            <GrowiContextualSubNavigation currentPage={pageWithMeta?.data} isLinkSharingDisabled={props.disableLinkSharing} />
           </div>
           </div>
         </header>
         </header>
         <div className="d-edit-none">
         <div className="d-edit-none">
@@ -392,6 +397,21 @@ class MultiplePagesHitsError extends ExtensibleCustomError {
 
 
 }
 }
 
 
+// apply parent page grant fot creating page
+async function applyGrantToPage(props: Props, ancestor: any) {
+  await ancestor.populate('grantedGroup');
+  const grant = {
+    grant: ancestor.grant,
+  };
+  const grantedGroup = ancestor.grantedGroup ? {
+    grantedGroup: {
+      id: ancestor.grantedGroup.id,
+      name: ancestor.grantedGroup.name,
+    },
+  } : {};
+  props.grantData = Object.assign(grant, grantedGroup);
+}
+
 async function injectPageData(context: GetServerSidePropsContext, props: Props): Promise<void> {
 async function injectPageData(context: GetServerSidePropsContext, props: Props): Promise<void> {
   const { model: mongooseModel } = await import('mongoose');
   const { model: mongooseModel } = await import('mongoose');
 
 
@@ -449,6 +469,12 @@ async function injectPageData(context: GetServerSidePropsContext, props: Props):
       props.templateTagData = templateData.templateTags as string[];
       props.templateTagData = templateData.templateTags as string[];
       props.templateBodyData = templateData.templateBody as string;
       props.templateBodyData = templateData.templateBody as string;
     }
     }
+
+    // apply pagrent page grant
+    const ancestor = await Page.findAncestorByPathAndViewer(currentPathname, user);
+    if (ancestor != null) {
+      await applyGrantToPage(props, ancestor);
+    }
   }
   }
 
 
   props.pageWithMeta = pageWithMeta;
   props.pageWithMeta = pageWithMeta;

+ 2 - 2
packages/app/src/pages/_app.page.tsx

@@ -11,7 +11,7 @@ import * as nextI18nConfig from '^/config/next-i18next.config';
 import { ActivatePluginService } from '~/client/services/activate-plugin';
 import { ActivatePluginService } from '~/client/services/activate-plugin';
 import { useI18nextHMR } from '~/services/i18next-hmr';
 import { useI18nextHMR } from '~/services/i18next-hmr';
 import {
 import {
-  useAppTitle, useConfidential, useGrowiVersion, useSiteUrl, useCustomizedLogoSrc,
+  useAppTitle, useConfidential, useGrowiVersion, useSiteUrl, useIsDefaultLogo,
 } from '~/stores/context';
 } from '~/stores/context';
 import { SWRConfigValue, swrGlobalConfiguration } from '~/utils/swr-utils';
 import { SWRConfigValue, swrGlobalConfiguration } from '~/utils/swr-utils';
 
 
@@ -65,7 +65,7 @@ function GrowiApp({ Component, pageProps }: GrowiAppProps): JSX.Element {
   useSiteUrl(commonPageProps.siteUrl);
   useSiteUrl(commonPageProps.siteUrl);
   useConfidential(commonPageProps.confidential);
   useConfidential(commonPageProps.confidential);
   useGrowiVersion(commonPageProps.growiVersion);
   useGrowiVersion(commonPageProps.growiVersion);
-  useCustomizedLogoSrc(commonPageProps.customizedLogoSrc);
+  useIsDefaultLogo(commonPageProps.isDefaultLogo);
 
 
   // Use the layout defined at the page level, if available
   // Use the layout defined at the page level, if available
   const getLayout = Component.getLayout ?? (page => page);
   const getLayout = Component.getLayout ?? (page => page);

+ 4 - 1
packages/app/src/pages/_search.page.tsx

@@ -5,6 +5,7 @@ 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';
 
 
+import { useTranslation } from 'next-i18next';
 import SearchResultLayout from '~/components/Layout/SearchResultLayout';
 import SearchResultLayout from '~/components/Layout/SearchResultLayout';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
@@ -57,6 +58,8 @@ type Props = CommonProps & {
 const SearchResultPage: NextPageWithLayout<Props> = (props: Props) => {
 const SearchResultPage: NextPageWithLayout<Props> = (props: Props) => {
   const { userUISettings } = props;
   const { userUISettings } = props;
 
 
+  const { t } = useTranslation();
+
   // commons
   // commons
   useCsrfToken(props.csrfToken);
   useCsrfToken(props.csrfToken);
 
 
@@ -88,7 +91,7 @@ const SearchResultPage: NextPageWithLayout<Props> = (props: Props) => {
     return <PutbackPageModal />;
     return <PutbackPageModal />;
   };
   };
 
 
-  const title = generateCustomTitle(props, 'GROWI');
+  const title = generateCustomTitle(props, t('search_result.title'));
 
 
   return (
   return (
     <>
     <>

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

@@ -2,9 +2,8 @@ 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, generateCustomTitle } from '~/pages/utils/commons';
+import { CommonProps } from '~/pages/utils/commons';
 import { useCurrentUser } from '~/stores/context';
 import { useCurrentUser } from '~/stores/context';
 import { useIsMaintenanceMode } from '~/stores/maintenanceMode';
 import { useIsMaintenanceMode } from '~/stores/maintenanceMode';
 
 
@@ -18,13 +17,8 @@ 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>
   );
   );

+ 4 - 1
packages/app/src/pages/admin/customize.page.tsx

@@ -10,7 +10,7 @@ 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, generateCustomTitle } from '~/pages/utils/commons';
 import { CommonProps, generateCustomTitle } from '~/pages/utils/commons';
-import { useCustomizeTitle, useCurrentUser } from '~/stores/context';
+import { useCustomizeTitle, useCurrentUser, useIsCustomizedLogoUploaded } from '~/stores/context';
 
 
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
 
@@ -20,6 +20,7 @@ const CustomizeSettingContents = dynamic(() => import('~/components/Admin/Custom
 
 
 type Props = CommonProps & {
 type Props = CommonProps & {
   customizeTitle: string,
   customizeTitle: string,
+  isCustomizedLogoUploaded: boolean,
 };
 };
 
 
 
 
@@ -27,6 +28,7 @@ const AdminCustomizeSettingsPage: NextPage<Props> = (props) => {
   const { t } = useTranslation('admin');
   const { t } = useTranslation('admin');
   useCustomizeTitle(props.customizeTitle);
   useCustomizeTitle(props.customizeTitle);
   useCurrentUser(props.currentUser ?? null);
   useCurrentUser(props.currentUser ?? null);
+  useIsCustomizedLogoUploaded(props.isCustomizedLogoUploaded);
 
 
   const componentTitle = t('customize_settings.customize_settings');
   const componentTitle = t('customize_settings.customize_settings');
   const pageTitle = generateCustomTitle(props, componentTitle);
   const pageTitle = generateCustomTitle(props, componentTitle);
@@ -57,6 +59,7 @@ const injectServerConfigurations = async(context: GetServerSidePropsContext, pro
   const { crowi } = req;
   const { crowi } = req;
 
 
   props.customizeTitle = crowi.configManager.getConfig('crowi', 'customize:title');
   props.customizeTitle = crowi.configManager.getConfig('crowi', 'customize:title');
+  props.isCustomizedLogoUploaded = await crowi.attachmentService.isBrandLogoExist();
 };
 };
 
 
 export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
 export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {

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

@@ -35,7 +35,8 @@ const AdminHomePage: NextPage<Props> = (props) => {
 
 
   const { t } = useTranslation('admin');
   const { t } = useTranslation('admin');
 
 
-  const title = t('wiki_management_home_page');
+  const title = generateCustomTitle(props, t('wiki_management_home_page'));
+
   const injectableContainers: Container<any>[] = [];
   const injectableContainers: Container<any>[] = [];
 
 
   if (isClient()) {
   if (isClient()) {

+ 3 - 1
packages/app/src/pages/installer.page.tsx

@@ -4,6 +4,7 @@ import { pagePathUtils } from '@growi/core';
 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 Head from 'next/head';
 import Head from 'next/head';
 
 
@@ -33,6 +34,7 @@ type Props = CommonProps & {
 };
 };
 
 
 const InstallerPage: NextPage<Props> = (props: Props) => {
 const InstallerPage: NextPage<Props> = (props: Props) => {
+  const { t } = useTranslation();
 
 
   // commons
   // commons
   useAppTitle(props.appTitle);
   useAppTitle(props.appTitle);
@@ -40,7 +42,7 @@ const InstallerPage: NextPage<Props> = (props: Props) => {
   useConfidential(props.confidential);
   useConfidential(props.confidential);
   useCsrfToken(props.csrfToken);
   useCsrfToken(props.csrfToken);
 
 
-  const title = generateCustomTitle(props, 'GROWI');
+  const title = generateCustomTitle(props, t('installer.title'));
   const classNames: string[] = [];
   const classNames: string[] = [];
 
 
   return (
   return (

+ 3 - 1
packages/app/src/pages/invited.page.tsx

@@ -3,6 +3,7 @@ import React from 'react';
 import type { IUserHasId, IUser } from '@growi/core';
 import type { IUserHasId, IUser } from '@growi/core';
 import { USER_STATUS } from '@growi/core';
 import { USER_STATUS } from '@growi/core';
 import { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
 import { NextPage, GetServerSideProps, GetServerSidePropsContext } 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';
@@ -26,12 +27,13 @@ type Props = CommonProps & {
 }
 }
 
 
 const InvitedPage: NextPage<Props> = (props: Props) => {
 const InvitedPage: NextPage<Props> = (props: Props) => {
+  const { t } = useTranslation();
 
 
   useCsrfToken(props.csrfToken);
   useCsrfToken(props.csrfToken);
   useCurrentPathname(props.currentPathname);
   useCurrentPathname(props.currentPathname);
   useCurrentUser(props.currentUser);
   useCurrentUser(props.currentUser);
 
 
-  const title = generateCustomTitle(props, 'GROWI');
+  const title = generateCustomTitle(props, t('invited.title'));
   const classNames: string[] = ['invited-page'];
   const classNames: string[] = ['invited-page'];
 
 
   return (
   return (

+ 3 - 2
packages/app/src/pages/login/index.page.tsx

@@ -3,6 +3,7 @@ import React from 'react';
 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 Head from 'next/head';
 import Head from 'next/head';
 
 
@@ -19,7 +20,6 @@ import {
   useCurrentPathname,
   useCurrentPathname,
 } from '~/stores/context';
 } from '~/stores/context';
 
 
-
 import styles from './index.module.scss';
 import styles from './index.module.scss';
 
 
 
 
@@ -38,6 +38,7 @@ type Props = CommonProps & {
 };
 };
 
 
 const LoginPage: NextPage<Props> = (props: Props) => {
 const LoginPage: NextPage<Props> = (props: Props) => {
+  const { t } = useTranslation();
 
 
   // commons
   // commons
   useCsrfToken(props.csrfToken);
   useCsrfToken(props.csrfToken);
@@ -45,7 +46,7 @@ const LoginPage: NextPage<Props> = (props: Props) => {
   // page
   // page
   useCurrentPathname(props.currentPathname);
   useCurrentPathname(props.currentPathname);
 
 
-  const title = generateCustomTitle(props, 'GROWI');
+  const title = generateCustomTitle(props, t('login.title'));
   const classNames: string[] = ['login-page', styles['login-page']];
   const classNames: string[] = ['login-page', styles['login-page']];
 
 
   return (
   return (

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

@@ -111,7 +111,7 @@ const MePage: NextPageWithLayout<Props> = (props: Props) => {
 
 
   useRendererConfig(props.rendererConfig);
   useRendererConfig(props.rendererConfig);
 
 
-  const title = generateCustomTitle(props, 'GROWI');
+  const title = generateCustomTitle(props, targetPage.title);
 
 
   return (
   return (
     <>
     <>

+ 27 - 7
packages/app/src/pages/share/[[...path]].page.tsx

@@ -1,12 +1,13 @@
 import React from 'react';
 import React from 'react';
 
 
-import { IUserHasId } from '@growi/core';
+import { IUserHasId, IPagePopulatedToShowRevision } from '@growi/core';
 import {
 import {
   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';
 import Head from 'next/head';
 import Head from 'next/head';
+import superjson from 'superjson';
 
 
 import { useCurrentGrowiLayoutFluidClassName } from '~/client/services/layout';
 import { useCurrentGrowiLayoutFluidClassName } from '~/client/services/layout';
 import { MainPane } from '~/components/Layout/MainPane';
 import { MainPane } from '~/components/Layout/MainPane';
@@ -19,6 +20,7 @@ import { SupportedAction, SupportedActionType } from '~/interfaces/activity';
 import { CrowiRequest } from '~/interfaces/crowi-request';
 import { CrowiRequest } from '~/interfaces/crowi-request';
 import { RendererConfig } from '~/interfaces/services/renderer';
 import { RendererConfig } from '~/interfaces/services/renderer';
 import { IShareLinkHasId } from '~/interfaces/share-link';
 import { IShareLinkHasId } from '~/interfaces/share-link';
+import type { PageDocument } from '~/server/models/page';
 import {
 import {
   useCurrentUser, useCurrentPathname, useCurrentPageId, useRendererConfig, useIsSearchPage,
   useCurrentUser, useCurrentPathname, useCurrentPageId, useRendererConfig, useIsSearchPage,
   useShareLinkId, useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsSearchScopeChildrenAsDefault, useDrawioUri, useIsContainerFluid,
   useShareLinkId, useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsSearchScopeChildrenAsDefault, useDrawioUri, useIsContainerFluid,
@@ -27,7 +29,7 @@ import loggerFactory from '~/utils/logger';
 
 
 import { NextPageWithLayout } from '../_app.page';
 import { NextPageWithLayout } from '../_app.page';
 import {
 import {
-  CommonProps, getServerSideCommonProps, generateCustomTitle, getNextI18NextConfig,
+  CommonProps, getServerSideCommonProps, generateCustomTitleForPage, getNextI18NextConfig,
 } from '../utils/commons';
 } from '../utils/commons';
 
 
 const logger = loggerFactory('growi:next-page:share');
 const logger = loggerFactory('growi:next-page:share');
@@ -38,6 +40,7 @@ const ShareLinkAlert = dynamic(() => import('~/components/Page/ShareLinkAlert'),
 const ForbiddenPage = dynamic(() => import('~/components/ForbiddenPage'), { ssr: false });
 const ForbiddenPage = dynamic(() => import('~/components/ForbiddenPage'), { ssr: false });
 
 
 type Props = CommonProps & {
 type Props = CommonProps & {
+  shareLinkRelatedPage?: IShareLinkRelatedPage,
   shareLink?: IShareLinkHasId,
   shareLink?: IShareLinkHasId,
   isExpired: boolean,
   isExpired: boolean,
   disableLinkSharing: boolean,
   disableLinkSharing: boolean,
@@ -48,12 +51,29 @@ type Props = CommonProps & {
   rendererConfig: RendererConfig,
   rendererConfig: RendererConfig,
 };
 };
 
 
+type IShareLinkRelatedPage = IPagePopulatedToShowRevision & PageDocument;
+
+superjson.registerCustom<IShareLinkRelatedPage, string>(
+  {
+    isApplicable: (v): v is IShareLinkRelatedPage => {
+      return v != null
+        && v.toObject != null
+        && v.lastUpdateUser != null
+        && v.creator != null
+        && v.revision != null;
+    },
+    serialize: (v) => { return superjson.stringify(v.toObject()) },
+    deserialize: (v) => { return superjson.parse(v) },
+  },
+  'IShareLinkRelatedPageTransformer',
+);
+
 const SharedPage: NextPageWithLayout<Props> = (props: Props) => {
 const SharedPage: NextPageWithLayout<Props> = (props: Props) => {
   useIsSearchPage(false);
   useIsSearchPage(false);
   useShareLinkId(props.shareLink?._id);
   useShareLinkId(props.shareLink?._id);
   useCurrentPageId(props.shareLink?.relatedPage._id);
   useCurrentPageId(props.shareLink?.relatedPage._id);
   useCurrentUser(props.currentUser);
   useCurrentUser(props.currentUser);
-  useCurrentPathname(props.currentPathname);
+  const { data: currentPathname } = useCurrentPathname(props.currentPathname);
   useRendererConfig(props.rendererConfig);
   useRendererConfig(props.rendererConfig);
   useIsSearchServiceConfigured(props.isSearchServiceConfigured);
   useIsSearchServiceConfigured(props.isSearchServiceConfigured);
   useIsSearchServiceReachable(props.isSearchServiceReachable);
   useIsSearchServiceReachable(props.isSearchServiceReachable);
@@ -68,7 +88,7 @@ const SharedPage: NextPageWithLayout<Props> = (props: Props) => {
   const isShowSharedPage = !props.disableLinkSharing && !isNotFound && !props.isExpired;
   const isShowSharedPage = !props.disableLinkSharing && !isNotFound && !props.isExpired;
   const shareLink = props.shareLink;
   const shareLink = props.shareLink;
 
 
-  const title = generateCustomTitle(props, 'GROWI');
+  const title = generateCustomTitleForPage(props, currentPathname ?? '');
 
 
 
 
   const sideContents = shareLink != null
   const sideContents = shareLink != null
@@ -91,7 +111,7 @@ const SharedPage: NextPageWithLayout<Props> = (props: Props) => {
 
 
       <div className={`dynamic-layout-root ${growiLayoutFluidClass} h-100 d-flex flex-column justify-content-between`}>
       <div className={`dynamic-layout-root ${growiLayoutFluidClass} h-100 d-flex flex-column justify-content-between`}>
         <header className="py-0 position-relative">
         <header className="py-0 position-relative">
-          {isShowSharedPage && <GrowiContextualSubNavigation isLinkSharingDisabled={props.disableLinkSharing} />}
+          {isShowSharedPage && <GrowiContextualSubNavigation currentPage={props.shareLinkRelatedPage} isLinkSharingDisabled={props.disableLinkSharing} />}
         </header>
         </header>
 
 
         <div id="grw-fav-sticky-trigger" className="sticky-top"></div>
         <div id="grw-fav-sticky-trigger" className="sticky-top"></div>
@@ -128,7 +148,7 @@ const SharedPage: NextPageWithLayout<Props> = (props: Props) => {
           {(isShowSharedPage && shareLink != null) && (
           {(isShowSharedPage && shareLink != null) && (
             <>
             <>
               <ShareLinkAlert expiredAt={shareLink.expiredAt} createdAt={shareLink.createdAt} />
               <ShareLinkAlert expiredAt={shareLink.expiredAt} createdAt={shareLink.createdAt} />
-              <Page />
+              <Page currentPage={props.shareLinkRelatedPage} />
             </>
             </>
           )}
           )}
         </MainPane>
         </MainPane>
@@ -227,7 +247,7 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
     const ShareLinkModel = crowi.model('ShareLink');
     const ShareLinkModel = crowi.model('ShareLink');
     const shareLink = await ShareLinkModel.findOne({ _id: params.linkId }).populate('relatedPage');
     const shareLink = await ShareLinkModel.findOne({ _id: params.linkId }).populate('relatedPage');
     if (shareLink != null) {
     if (shareLink != null) {
-      await shareLink.relatedPage.populateDataToShowRevision();
+      props.shareLinkRelatedPage = await shareLink.relatedPage.populateDataToShowRevision();
       props.isExpired = shareLink.isExpired();
       props.isExpired = shareLink.isExpired();
       props.shareLink = shareLink.toObject();
       props.shareLink = shareLink.toObject();
     }
     }

+ 1 - 1
packages/app/src/pages/tags.page.tsx

@@ -80,7 +80,7 @@ const TagPage: NextPageWithLayout<CommonProps> = (props: Props) => {
 
 
   useRendererConfig(props.rendererConfig);
   useRendererConfig(props.rendererConfig);
 
 
-  const title = generateCustomTitle(props, 'GROWI');
+  const title = generateCustomTitle(props, t('Tags'));
 
 
   return (
   return (
     <>
     <>

+ 5 - 2
packages/app/src/pages/trash.page.tsx

@@ -26,8 +26,9 @@ import {
 
 
 import { NextPageWithLayout } from './_app.page';
 import { NextPageWithLayout } from './_app.page';
 import {
 import {
-  CommonProps, getServerSideCommonProps, getNextI18NextConfig, generateCustomTitle,
+  CommonProps, getServerSideCommonProps, getNextI18NextConfig, generateCustomTitleForPage,
 } from './utils/commons';
 } from './utils/commons';
+import { useTranslation } from 'next-i18next';
 
 
 const TrashPageList = dynamic(() => import('~/components/TrashPageList').then(mod => mod.TrashPageList), { ssr: false });
 const TrashPageList = dynamic(() => import('~/components/TrashPageList').then(mod => mod.TrashPageList), { ssr: false });
 const EmptyTrashModal = dynamic(() => import('~/components/EmptyTrashModal'), { ssr: false });
 const EmptyTrashModal = dynamic(() => import('~/components/EmptyTrashModal'), { ssr: false });
@@ -70,12 +71,14 @@ const TrashPage: NextPageWithLayout<CommonProps> = (props: Props) => {
 
 
   useRendererConfig(props.rendererConfig);
   useRendererConfig(props.rendererConfig);
 
 
+  const { t } = useTranslation();
+
   const { data: isDrawerMode } = useDrawerMode();
   const { data: isDrawerMode } = useDrawerMode();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
 
 
   const growiLayoutFluidClass = useCurrentGrowiLayoutFluidClassName();
   const growiLayoutFluidClass = useCurrentGrowiLayoutFluidClassName();
 
 
-  const title = generateCustomTitle(props, 'GROWI');
+  const title = generateCustomTitleForPage(props, '/trash');
 
 
   return (
   return (
     <>
     <>

+ 3 - 1
packages/app/src/pages/user-activation.page.tsx

@@ -1,4 +1,5 @@
 import { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
 import { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
+import { useTranslation } from 'next-i18next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import Head from 'next/head';
 import Head from 'next/head';
 
 
@@ -22,8 +23,9 @@ type Props = CommonProps & {
 }
 }
 
 
 const UserActivationPage: NextPage<Props> = (props: Props) => {
 const UserActivationPage: NextPage<Props> = (props: Props) => {
+  const { t } = useTranslation();
 
 
-  const title = generateCustomTitle(props, 'GROWI');
+  const title = generateCustomTitle(props, t('User Activation'));
 
 
   return (
   return (
     <NoLoginLayout>
     <NoLoginLayout>

+ 5 - 6
packages/app/src/pages/utils/commons.ts

@@ -20,7 +20,7 @@ export type CommonProps = {
   growiVersion: string,
   growiVersion: string,
   isMaintenanceMode: boolean,
   isMaintenanceMode: boolean,
   redirectDestination: string | null,
   redirectDestination: string | null,
-  customizedLogoSrc?: string,
+  isDefaultLogo: boolean,
   currentUser?: IUser,
   currentUser?: IUser,
 } & Partial<SSRConfig>;
 } & Partial<SSRConfig>;
 
 
@@ -30,7 +30,7 @@ export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(c
   const req = context.req as CrowiRequest<IUserHasId & any>;
   const req = context.req as CrowiRequest<IUserHasId & any>;
   const { crowi, user } = req;
   const { crowi, user } = req;
   const {
   const {
-    appService, configManager, customizeService,
+    appService, configManager, customizeService, attachmentService,
   } = crowi;
   } = crowi;
 
 
   const url = new URL(context.resolvedUrl, 'http://example.com');
   const url = new URL(context.resolvedUrl, 'http://example.com');
@@ -45,7 +45,8 @@ export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(c
 
 
   // eslint-disable-next-line max-len, no-nested-ternary
   // eslint-disable-next-line max-len, no-nested-ternary
   const redirectDestination = !isMaintenanceMode && currentPathname === '/maintenance' ? '/' : isMaintenanceMode && !currentPathname.match('/admin/*') && !(currentPathname === '/maintenance') ? '/maintenance' : null;
   const redirectDestination = !isMaintenanceMode && currentPathname === '/maintenance' ? '/' : isMaintenanceMode && !currentPathname.match('/admin/*') && !(currentPathname === '/maintenance') ? '/maintenance' : null;
-  const isDefaultLogo = crowi.configManager.getConfig('crowi', 'customize:isDefaultLogo');
+  const isCustomizedLogoUploaded = await attachmentService.isBrandLogoExist();
+  const isDefaultLogo = crowi.configManager.getConfig('crowi', 'customize:isDefaultLogo') || !isCustomizedLogoUploaded;
 
 
   const props: CommonProps = {
   const props: CommonProps = {
     namespacesRequired: ['translation'],
     namespacesRequired: ['translation'],
@@ -59,8 +60,8 @@ export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(c
     growiVersion: crowi.version,
     growiVersion: crowi.version,
     isMaintenanceMode,
     isMaintenanceMode,
     redirectDestination,
     redirectDestination,
-    customizedLogoSrc: isDefaultLogo ? null : configManager.getConfig('crowi', 'customize:customizedLogoSrc'),
     currentUser,
     currentUser,
+    isDefaultLogo,
   };
   };
 
 
   return { props };
   return { props };
@@ -104,7 +105,6 @@ export const getNextI18NextConfig = async(
 export const generateCustomTitle = (props: CommonProps, title: string): string => {
 export const generateCustomTitle = (props: CommonProps, title: string): string => {
   return props.customTitleTemplate
   return props.customTitleTemplate
     .replace('{{sitename}}', props.appTitle)
     .replace('{{sitename}}', props.appTitle)
-    .replace('{{page}}', title)
     .replace('{{pagepath}}', title)
     .replace('{{pagepath}}', title)
     .replace('{{pagename}}', title);
     .replace('{{pagename}}', title);
 };
 };
@@ -120,6 +120,5 @@ export const generateCustomTitleForPage = (props: CommonProps, pagePath: string)
   return props.customTitleTemplate
   return props.customTitleTemplate
     .replace('{{sitename}}', props.appTitle)
     .replace('{{sitename}}', props.appTitle)
     .replace('{{pagepath}}', pagePath)
     .replace('{{pagepath}}', pagePath)
-    .replace('{{page}}', dPagePath.latter) // for backward compatibility
     .replace('{{pagename}}', dPagePath.latter);
     .replace('{{pagename}}', dPagePath.latter);
 };
 };

+ 8 - 0
packages/app/src/server/middlewares/certify-brand-logo.ts

@@ -0,0 +1,8 @@
+export const generateCertifyBrandLogoMiddleware = (crowi) => {
+
+  return async(req, res, next) => {
+    req.isBrandLogo = true;
+    next();
+  };
+
+};

+ 6 - 0
packages/app/src/server/middlewares/login-required.js

@@ -43,6 +43,12 @@ module.exports = (crowi, isGuestAllowed = false, fallback = null) => {
       return next();
       return next();
     }
     }
 
 
+    // Check if it is a Brand logo
+    if (req.isBrandLogo) {
+      logger.debug('Target is Brand logo');
+      return next();
+    }
+
     // is api path
     // is api path
     const baseUrl = req.baseUrl || '';
     const baseUrl = req.baseUrl || '';
     if (baseUrl.match(/^\/_api\/.+$/)) {
     if (baseUrl.match(/^\/_api\/.+$/)) {

+ 0 - 3
packages/app/src/server/models/config.ts

@@ -241,9 +241,6 @@ schema.statics.getLocalconfig = function(crowi) {
     globalLang: crowi.configManager.getConfig('crowi', 'app:globalLang'),
     globalLang: crowi.configManager.getConfig('crowi', 'app:globalLang'),
     pageLimitationL: crowi.configManager.getConfig('crowi', 'customize:showPageLimitationL'),
     pageLimitationL: crowi.configManager.getConfig('crowi', 'customize:showPageLimitationL'),
     pageLimitationXL: crowi.configManager.getConfig('crowi', 'customize:showPageLimitationXL'),
     pageLimitationXL: crowi.configManager.getConfig('crowi', 'customize:showPageLimitationXL'),
-    customizedLogoSrc: isDefaultLogo != null && !isDefaultLogo
-      ? crowi.configManager.getConfig('crowi', 'customize:customizedLogoSrc')
-      : null,
     auditLogEnabled: crowi.configManager.getConfig('crowi', 'app:auditLogEnabled'),
     auditLogEnabled: crowi.configManager.getConfig('crowi', 'app:auditLogEnabled'),
     activityExpirationSeconds: crowi.configManager.getConfig('crowi', 'app:activityExpirationSeconds'),
     activityExpirationSeconds: crowi.configManager.getConfig('crowi', 'app:activityExpirationSeconds'),
     auditLogAvailableActions: crowi.activityService.getAvailableActions(false),
     auditLogAvailableActions: crowi.activityService.getAvailableActions(false),

+ 0 - 14
packages/app/src/server/routes/apiv3/customize-setting.js

@@ -660,12 +660,6 @@ module.exports = (crowi) => {
     }
     }
   });
   });
 
 
-  router.get('/customize-logo', loginRequiredStrictly, adminRequired, async(req, res) => {
-    const isDefaultLogo = await crowi.configManager.getConfig('crowi', 'customize:isDefaultLogo');
-    const customizedLogoSrc = await crowi.configManager.getConfig('crowi', 'customize:customizedLogoSrc');
-    return res.apiv3({ isDefaultLogo, customizedLogoSrc });
-  });
-
   router.put('/customize-logo', loginRequiredStrictly, adminRequired, validator.logo, apiV3FormValidator, async(req, res) => {
   router.put('/customize-logo', loginRequiredStrictly, adminRequired, validator.logo, apiV3FormValidator, async(req, res) => {
 
 
     const {
     const {
@@ -717,11 +711,6 @@ module.exports = (crowi) => {
       let attachment;
       let attachment;
       try {
       try {
         attachment = await attachmentService.createAttachment(file, req.user, null, AttachmentType.BRAND_LOGO);
         attachment = await attachmentService.createAttachment(file, req.user, null, AttachmentType.BRAND_LOGO);
-        const attachmentConfigParams = {
-          'customize:customizedLogoSrc': attachment.filePathProxied,
-        };
-
-        await crowi.configManager.updateConfigsInTheSameNamespace('crowi', attachmentConfigParams);
       }
       }
       catch (err) {
       catch (err) {
         logger.error(err);
         logger.error(err);
@@ -741,9 +730,6 @@ module.exports = (crowi) => {
 
 
     try {
     try {
       await attachmentService.removeAllAttachments(attachments);
       await attachmentService.removeAllAttachments(attachments);
-      // update attachmentId immediately
-      const attachmentConfigParams = { 'customize:customizedLogoSrc': null };
-      await crowi.configManager.updateConfigsInTheSameNamespace('crowi', attachmentConfigParams);
     }
     }
     catch (err) {
     catch (err) {
       logger.error(err);
       logger.error(err);

+ 10 - 0
packages/app/src/server/routes/attachment.js

@@ -296,6 +296,16 @@ module.exports = function(crowi, app) {
     return responseForAttachment(req, res, attachment);
     return responseForAttachment(req, res, attachment);
   };
   };
 
 
+  api.getBrandLogo = async function(req, res) {
+    const brandLogoAttachment = await Attachment.findOne({ attachmentType: AttachmentType.BRAND_LOGO });
+
+    if (brandLogoAttachment == null) {
+      return res.status(404).json(ApiResponse.error('Brand logo does not exist'));
+    }
+
+    return responseForAttachment(req, res, brandLogoAttachment);
+  };
+
   /**
   /**
    * @api {get} /attachments.obsoletedGetForMongoDB get attachments from mongoDB
    * @api {get} /attachments.obsoletedGetForMongoDB get attachments from mongoDB
    * @apiName get
    * @apiName get

+ 4 - 0
packages/app/src/server/routes/index.js

@@ -3,6 +3,7 @@ import express from 'express';
 
 
 import { generateAddActivityMiddleware } from '../middlewares/add-activity';
 import { generateAddActivityMiddleware } from '../middlewares/add-activity';
 import apiV1FormValidator from '../middlewares/apiv1-form-validator';
 import apiV1FormValidator from '../middlewares/apiv1-form-validator';
+import { generateCertifyBrandLogoMiddleware } from '../middlewares/certify-brand-logo';
 import injectResetOrderByTokenMiddleware from '../middlewares/inject-reset-order-by-token-middleware';
 import injectResetOrderByTokenMiddleware from '../middlewares/inject-reset-order-by-token-middleware';
 import injectUserRegistrationOrderByTokenMiddleware from '../middlewares/inject-user-registration-order-by-token-middleware';
 import injectUserRegistrationOrderByTokenMiddleware from '../middlewares/inject-user-registration-order-by-token-middleware';
 import * as loginFormValidator from '../middlewares/login-form-validator';
 import * as loginFormValidator from '../middlewares/login-form-validator';
@@ -30,6 +31,7 @@ module.exports = function(crowi, app) {
   const loginRequired = require('../middlewares/login-required')(crowi, true);
   const loginRequired = require('../middlewares/login-required')(crowi, true);
   const adminRequired = require('../middlewares/admin-required')(crowi);
   const adminRequired = require('../middlewares/admin-required')(crowi);
   const certifySharedFile = require('../middlewares/certify-shared-file')(crowi);
   const certifySharedFile = require('../middlewares/certify-shared-file')(crowi);
+  const certifyBrandLogo = generateCertifyBrandLogoMiddleware(crowi);
   const rateLimiter = require('../middlewares/rate-limiter')();
   const rateLimiter = require('../middlewares/rate-limiter')();
   const addActivity = generateAddActivityMiddleware(crowi);
   const addActivity = generateAddActivityMiddleware(crowi);
 
 
@@ -106,6 +108,8 @@ module.exports = function(crowi, app) {
   app.post('/_api/admin/import/qiita'           , loginRequiredStrictly , adminRequired , csrfProtection, addActivity, admin.api.importDataFromQiita);
   app.post('/_api/admin/import/qiita'           , loginRequiredStrictly , adminRequired , csrfProtection, addActivity, admin.api.importDataFromQiita);
   app.post('/_api/admin/import/testQiitaAPI'    , loginRequiredStrictly , adminRequired , csrfProtection, addActivity, admin.api.testQiitaAPI);
   app.post('/_api/admin/import/testQiitaAPI'    , loginRequiredStrictly , adminRequired , csrfProtection, addActivity, admin.api.testQiitaAPI);
 
 
+  app.get('/attachment/brand-logo' , certifyBrandLogo, loginRequired, attachment.api.getBrandLogo);
+
   /*
   /*
    * Routes below are unavailable when maintenance mode
    * Routes below are unavailable when maintenance mode
    */
    */

+ 11 - 0
packages/app/src/server/service/attachment.js

@@ -1,5 +1,7 @@
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
+import { AttachmentType } from '../interfaces/attachment';
+
 const fs = require('fs');
 const fs = require('fs');
 
 
 const mongoose = require('mongoose');
 const mongoose = require('mongoose');
@@ -77,6 +79,15 @@ class AttachmentService {
     return;
     return;
   }
   }
 
 
+  async isBrandLogoExist() {
+    const Attachment = this.crowi.model('Attachment');
+
+    const query = { attachmentType: AttachmentType.BRAND_LOGO };
+    const count = await Attachment.countDocuments(query);
+
+    return count >= 1;
+  }
+
 }
 }
 
 
 module.exports = AttachmentService;
 module.exports = AttachmentService;

+ 1 - 1
packages/app/src/services/renderer/rehype-plugins/add-line-number-attribute.ts

@@ -5,7 +5,7 @@ import { visit } from 'unist-util-visit';
 
 
 import { addClassToProperties } from './add-class';
 import { addClassToProperties } from './add-class';
 
 
-const REGEXP_TARGET_TAGNAMES = new RegExp(/^(h1|h2|h3|h4|h5|h6|p|img|pre|blockquote|hr|ol|ul)$/);
+const REGEXP_TARGET_TAGNAMES = new RegExp(/^(h1|h2|h3|h4|h5|h6|p|img|pre|blockquote|hr|ol|ul|table|tr)$/);
 
 
 export const rehypePlugin: Plugin = () => {
 export const rehypePlugin: Plugin = () => {
   return (tree) => {
   return (tree) => {

+ 5 - 2
packages/app/src/services/renderer/remark-plugins/plantuml.ts

@@ -1,12 +1,15 @@
 import plantuml from '@akebifiky/remark-simple-plantuml';
 import plantuml from '@akebifiky/remark-simple-plantuml';
 import { Plugin } from 'unified';
 import { Plugin } from 'unified';
+import urljoin from 'url-join';
 
 
 type PlantUMLPluginParams = {
 type PlantUMLPluginParams = {
-  baseUrl?: string,
+  plantumlUri?: string,
 }
 }
 
 
 export const remarkPlugin: Plugin<[PlantUMLPluginParams]> = (options) => {
 export const remarkPlugin: Plugin<[PlantUMLPluginParams]> = (options) => {
-  const baseUrl = options.baseUrl ?? 'https://www.plantuml.com/plantuml/svg';
+  const plantumlUri = options.plantumlUri ?? 'https://www.plantuml.com/plantuml';
+
+  const baseUrl = urljoin(plantumlUri, '/svg');
 
 
   return plantuml.bind(this)({ baseUrl });
   return plantuml.bind(this)({ baseUrl });
 };
 };

+ 3 - 3
packages/app/src/services/renderer/renderer.tsx

@@ -139,7 +139,7 @@ export const generateViewOptions = (
   // add remark plugins
   // add remark plugins
   remarkPlugins.push(
   remarkPlugins.push(
     math,
     math,
-    [plantuml.remarkPlugin, { baseUrl: config.plantumlUri }],
+    [plantuml.remarkPlugin, { plantumlUri: config.plantumlUri }],
     drawioPlugin.remarkPlugin,
     drawioPlugin.remarkPlugin,
     xsvToTable.remarkPlugin,
     xsvToTable.remarkPlugin,
     lsxGrowiPlugin.remarkPlugin,
     lsxGrowiPlugin.remarkPlugin,
@@ -221,7 +221,7 @@ export const generateSimpleViewOptions = (
   // add remark plugins
   // add remark plugins
   remarkPlugins.push(
   remarkPlugins.push(
     math,
     math,
-    [plantuml.remarkPlugin, { baseUrl: config.plantumlUri }],
+    [plantuml.remarkPlugin, { plantumlUri: config.plantumlUri }],
     drawioPlugin.remarkPlugin,
     drawioPlugin.remarkPlugin,
     xsvToTable.remarkPlugin,
     xsvToTable.remarkPlugin,
     lsxGrowiPlugin.remarkPlugin,
     lsxGrowiPlugin.remarkPlugin,
@@ -271,7 +271,7 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
   // add remark plugins
   // add remark plugins
   remarkPlugins.push(
   remarkPlugins.push(
     math,
     math,
-    [plantuml.remarkPlugin, { baseUrl: config.plantumlUri }],
+    [plantuml.remarkPlugin, { plantumlUri: config.plantumlUri }],
     drawioPlugin.remarkPlugin,
     drawioPlugin.remarkPlugin,
     xsvToTable.remarkPlugin,
     xsvToTable.remarkPlugin,
     lsxGrowiPlugin.remarkPlugin,
     lsxGrowiPlugin.remarkPlugin,

+ 8 - 4
packages/app/src/stores/context.tsx

@@ -76,8 +76,8 @@ export const useIsSharedUser = (initialData?: boolean): SWRResponse<boolean, Err
   return useContextSWR<boolean, Error>('isSharedUser', initialData);
   return useContextSWR<boolean, Error>('isSharedUser', initialData);
 };
 };
 
 
-export const useShareLinkId = (initialData?: Nullable<string>): SWRResponse<Nullable<string>, Error> => {
-  return useContextSWR<Nullable<string>, Error>('shareLinkId', initialData);
+export const useShareLinkId = (initialData?: string): SWRResponse<string, Error> => {
+  return useContextSWR('shareLinkId', initialData);
 };
 };
 
 
 export const useDisableLinkSharing = (initialData?: Nullable<boolean>): SWRResponse<Nullable<boolean>, Error> => {
 export const useDisableLinkSharing = (initialData?: Nullable<boolean>): SWRResponse<Nullable<boolean>, Error> => {
@@ -201,8 +201,12 @@ export const useCustomizeTitle = (initialData?: string): SWRResponse<string, Err
   return useContextSWR('CustomizeTitle', initialData);
   return useContextSWR('CustomizeTitle', initialData);
 };
 };
 
 
-export const useCustomizedLogoSrc = (initialData?: string): SWRResponse<string, Error> => {
-  return useContextSWR('customizedLogoSrc', initialData);
+export const useIsDefaultLogo = (initialData?: boolean): SWRResponse<boolean, Error> => {
+  return useContextSWR('isDefaultLogo', initialData);
+};
+
+export const useIsCustomizedLogoUploaded = (initialData?: boolean): SWRResponse<boolean, Error> => {
+  return useStaticSWR('isCustomizedLogoUploaded', initialData);
 };
 };
 
 
 export const useGrowiCloudUri = (initialData?: string): SWRResponse<string, Error> => {
 export const useGrowiCloudUri = (initialData?: string): SWRResponse<string, Error> => {

+ 1 - 1
packages/app/src/stores/editor.tsx

@@ -89,7 +89,7 @@ export const useCurrentIndentSize = (): SWRResponse<number, Error> => {
 */
 */
 export const useSWRxSlackChannels = (currentPagePath: Nullable<string>): SWRResponse<string[], Error> => {
 export const useSWRxSlackChannels = (currentPagePath: Nullable<string>): SWRResponse<string[], Error> => {
   const shouldFetch: boolean = currentPagePath != null;
   const shouldFetch: boolean = currentPagePath != null;
-  return useSWR(
+  return useSWRImmutable(
     shouldFetch ? ['/pages.updatePost', currentPagePath] : null,
     shouldFetch ? ['/pages.updatePost', currentPagePath] : null,
     (endpoint, path) => apiGet(endpoint, { path }).then((response: SlackChannels) => response.updatePost),
     (endpoint, path) => apiGet(endpoint, { path }).then((response: SlackChannels) => response.updatePost),
     { fallbackData: [''] },
     { fallbackData: [''] },

+ 3 - 1
packages/app/src/stores/in-app-notification.ts

@@ -1,4 +1,4 @@
-import useSWR, { SWRResponse } from 'swr';
+import useSWR, { SWRConfiguration, SWRResponse } from 'swr';
 
 
 import type { InAppNotificationStatuses, IInAppNotification, PaginateResult } from '~/interfaces/in-app-notification';
 import type { InAppNotificationStatuses, IInAppNotification, PaginateResult } from '~/interfaces/in-app-notification';
 import { parseSnapshot } from '~/models/serializers/in-app-notification-snapshot/page';
 import { parseSnapshot } from '~/models/serializers/in-app-notification-snapshot/page';
@@ -15,6 +15,7 @@ export const useSWRxInAppNotifications = <Data, Error>(
   limit: number,
   limit: number,
   offset?: number,
   offset?: number,
   status?: InAppNotificationStatuses,
   status?: InAppNotificationStatuses,
+  config?: SWRConfiguration,
 ): SWRResponse<PaginateResult<IInAppNotification>, Error> => {
 ): SWRResponse<PaginateResult<IInAppNotification>, Error> => {
   return useSWR(
   return useSWR(
     ['/in-app-notification/list', limit, offset, status],
     ['/in-app-notification/list', limit, offset, status],
@@ -30,6 +31,7 @@ export const useSWRxInAppNotifications = <Data, Error>(
       });
       });
       return inAppNotificationPaginateResult;
       return inAppNotificationPaginateResult;
     }),
     }),
+    config,
   );
   );
 };
 };
 
 

+ 23 - 20
packages/app/src/stores/page.tsx

@@ -4,7 +4,7 @@ import type {
   IPageInfoForEntity, IPagePopulatedToShowRevision, Nullable,
   IPageInfoForEntity, IPagePopulatedToShowRevision, Nullable,
 } from '@growi/core';
 } from '@growi/core';
 import { isClient, pagePathUtils } from '@growi/core';
 import { isClient, pagePathUtils } from '@growi/core';
-import useSWR, { Key, SWRResponse } from 'swr';
+import useSWR, { Key, SWRConfiguration, SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 import useSWRImmutable from 'swr/immutable';
 
 
 import { apiGet } from '~/client/util/apiv1-client';
 import { apiGet } from '~/client/util/apiv1-client';
@@ -17,20 +17,22 @@ import { IRevisionsForPagination } from '~/interfaces/revision';
 
 
 import { IPageTagsInfo } from '../interfaces/tag';
 import { IPageTagsInfo } from '../interfaces/tag';
 
 
-import { useCurrentPageId, useCurrentPathname } from './context';
+import { useCurrentPageId, useCurrentPathname, useShareLinkId } from './context';
 import { ITermNumberManagerUtil, useTermNumberManager } from './use-static-swr';
 import { ITermNumberManagerUtil, useTermNumberManager } from './use-static-swr';
 
 
 const { isPermalink: _isPermalink } = pagePathUtils;
 const { isPermalink: _isPermalink } = pagePathUtils;
 
 
-
 export const useSWRxPage = (
 export const useSWRxPage = (
     pageId?: string|null,
     pageId?: string|null,
     shareLinkId?: string,
     shareLinkId?: string,
     revisionId?: string,
     revisionId?: string,
     initialData?: IPagePopulatedToShowRevision|null,
     initialData?: IPagePopulatedToShowRevision|null,
+    config?: SWRConfiguration,
 ): SWRResponse<IPagePopulatedToShowRevision|null, Error> => {
 ): SWRResponse<IPagePopulatedToShowRevision|null, Error> => {
   const swrResponse = useSWRImmutable<IPagePopulatedToShowRevision|null, Error>(
   const swrResponse = useSWRImmutable<IPagePopulatedToShowRevision|null, Error>(
     pageId != null ? ['/page', pageId, shareLinkId, revisionId] : null,
     pageId != null ? ['/page', pageId, shareLinkId, revisionId] : null,
+    // TODO: upgrade SWR to v2 and use useSWRMutation
+    //        in order to trigger mutation manually
     (endpoint, pageId, shareLinkId, revisionId) => apiv3Get<{ page: IPagePopulatedToShowRevision }>(endpoint, { pageId, shareLinkId, revisionId })
     (endpoint, pageId, shareLinkId, revisionId) => apiv3Get<{ page: IPagePopulatedToShowRevision }>(endpoint, { pageId, shareLinkId, revisionId })
       .then(result => result.data.page)
       .then(result => result.data.page)
       .catch((errs) => {
       .catch((errs) => {
@@ -42,6 +44,7 @@ export const useSWRxPage = (
         }
         }
         throw Error('failed to get page');
         throw Error('failed to get page');
       }),
       }),
+    config,
   );
   );
 
 
   useEffect(() => {
   useEffect(() => {
@@ -61,10 +64,9 @@ export const useSWRxPageByPath = (path?: string): SWRResponse<IPagePopulatedToSh
   );
   );
 };
 };
 
 
-export const useSWRxCurrentPage = (
-    shareLinkId?: string, initialData?: IPagePopulatedToShowRevision|null,
-): SWRResponse<IPagePopulatedToShowRevision|null, Error> => {
+export const useSWRxCurrentPage = (initialData?: IPagePopulatedToShowRevision|null): SWRResponse<IPagePopulatedToShowRevision|null, Error> => {
   const { data: currentPageId } = useCurrentPageId();
   const { data: currentPageId } = useCurrentPageId();
+  const { data: shareLinkId } = useShareLinkId();
 
 
   // Get URL parameter for specific revisionId
   // Get URL parameter for specific revisionId
   let revisionId: string|undefined;
   let revisionId: string|undefined;
@@ -74,29 +76,30 @@ export const useSWRxCurrentPage = (
     revisionId = requestRevisionId != null ? requestRevisionId : undefined;
     revisionId = requestRevisionId != null ? requestRevisionId : undefined;
   }
   }
 
 
-  const swrResult = useSWRxPage(currentPageId, shareLinkId, revisionId, initialData);
+  const swrResult = useSWRxPage(
+    currentPageId, shareLinkId, revisionId,
+    initialData,
+    // overwrite fetcher if the current page is share link
+    shareLinkId == null
+      ? undefined
+      : {
+        fetcher: () => null,
+      },
+  );
 
 
   return swrResult;
   return swrResult;
 };
 };
 
 
 
 
 export const useSWRxTagsInfo = (pageId: Nullable<string>): SWRResponse<IPageTagsInfo | undefined, Error> => {
 export const useSWRxTagsInfo = (pageId: Nullable<string>): SWRResponse<IPageTagsInfo | undefined, Error> => {
+  const { data: shareLinkId } = useShareLinkId();
 
 
   const endpoint = `/pages.getPageTag?pageId=${pageId}`;
   const endpoint = `/pages.getPageTag?pageId=${pageId}`;
-  const key = [endpoint, pageId];
-
-  const fetcher = async(endpoint: string, pageId: Nullable<string>) => {
-    let tags: string[] = [];
-    // when the page exists
-    if (pageId != null) {
-      const res = await apiGet<IPageTagsInfo>(endpoint, { pageId });
-      tags = res?.tags;
-    }
-
-    return { tags };
-  };
 
 
-  return useSWRImmutable(key, fetcher);
+  return useSWRImmutable<IPageTagsInfo | undefined, Error>(
+    shareLinkId == null && pageId != null ? [endpoint, pageId] : null,
+    (endpoint, pageId) => apiGet<IPageTagsInfo>(endpoint, { pageId }).then(result => result),
+  );
 };
 };
 
 
 export const usePageInfoTermManager = (isDisabled?: boolean) : SWRResponse<number, Error> & ITermNumberManagerUtil => {
 export const usePageInfoTermManager = (isDisabled?: boolean) : SWRResponse<number, Error> & ITermNumberManagerUtil => {

+ 5 - 4
packages/app/src/stores/personal-settings.tsx

@@ -1,5 +1,5 @@
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
-import useSWR, { SWRResponse } from 'swr';
+import useSWR, { SWRConfiguration, SWRResponse } from 'swr';
 
 
 
 
 import { IExternalAccount } from '~/interfaces/external-account';
 import { IExternalAccount } from '~/interfaces/external-account';
@@ -13,10 +13,11 @@ import { useStaticSWR } from './use-static-swr';
 const logger = loggerFactory('growi:stores:personal-settings');
 const logger = loggerFactory('growi:stores:personal-settings');
 
 
 
 
-export const useSWRxPersonalSettings = (): SWRResponse<IUser, Error> => {
+export const useSWRxPersonalSettings = (config?: SWRConfiguration): SWRResponse<IUser, Error> => {
   return useSWR(
   return useSWR(
     '/personal-setting',
     '/personal-setting',
     endpoint => apiv3Get(endpoint).then(response => response.data.currentUser),
     endpoint => apiv3Get(endpoint).then(response => response.data.currentUser),
+    config,
   );
   );
 };
 };
 
 
@@ -27,9 +28,9 @@ export type IPersonalSettingsInfoOption = {
   disassociateLdapAccount: (account: { providerType: string, accountId: string }) => Promise<void>,
   disassociateLdapAccount: (account: { providerType: string, accountId: string }) => Promise<void>,
 }
 }
 
 
-export const usePersonalSettings = (): SWRResponse<IUser, Error> & IPersonalSettingsInfoOption => {
+export const usePersonalSettings = (config?: SWRConfiguration): SWRResponse<IUser, Error> & IPersonalSettingsInfoOption => {
   const { i18n } = useTranslation();
   const { i18n } = useTranslation();
-  const { data: personalSettingsDataFromDB, mutate: revalidate } = useSWRxPersonalSettings();
+  const { data: personalSettingsDataFromDB, mutate: revalidate } = useSWRxPersonalSettings(config);
   const key = personalSettingsDataFromDB != null ? 'personalSettingsInfo' : null;
   const key = personalSettingsDataFromDB != null ? 'personalSettingsInfo' : null;
 
 
   const swrResult = useStaticSWR<IUser, Error>(key, undefined, { fallbackData: personalSettingsDataFromDB });
   const swrResult = useStaticSWR<IUser, Error>(key, undefined, { fallbackData: personalSettingsDataFromDB });

+ 2 - 2
packages/app/src/stores/ui.tsx

@@ -465,8 +465,8 @@ export const useIsAbleToShowTagLabel = (): SWRResponse<boolean, Error> => {
   );
   );
 };
 };
 
 
-export const useIsAbleToShowPageEditorModeManager = (): SWRResponse<boolean, Error> => {
-  const key = 'isAbleToShowPageEditorModeManager';
+export const useIsAbleToChangeEditorMode = (): SWRResponse<boolean, Error> => {
+  const key = 'isAbleToChangeEditorMode';
   const { data: isEditable } = useIsEditable();
   const { data: isEditable } = useIsEditable();
   const { data: isSharedUser } = useIsSharedUser();
   const { data: isSharedUser } = useIsSharedUser();
 
 

+ 5 - 1
packages/app/src/stores/user.tsx

@@ -12,7 +12,11 @@ export const useSWRxUsersList = (userIds: string[]): SWRResponse<IUserHasId[], E
     (endpoint, userIds) => apiv3Get(endpoint, { userIds: userIds.join(',') }).then((response) => {
     (endpoint, userIds) => apiv3Get(endpoint, { userIds: userIds.join(',') }).then((response) => {
       return response.data.users;
       return response.data.users;
     }),
     }),
-    { use: [checkAndUpdateImageUrlCached] },
+    {
+      use: [checkAndUpdateImageUrlCached],
+      revalidateOnFocus: false,
+      revalidateOnReconnect: false,
+    },
   );
   );
 };
 };
 
 

+ 0 - 32
packages/app/src/styles/_layout.scss

@@ -71,38 +71,6 @@ body.not-found-page .grw-container-convertible {
   top: calc(100px + 4px + 20px);
   top: calc(100px + 4px + 20px);
 }
 }
 
 
-.grw-fab {
-  position: fixed;
-  right: 1.5rem;
-  bottom: 3rem;
-  z-index: bs.$zindex-fixed;
-
-  transition: all 200ms linear;
-
-  .btn-create-page {
-    width: 60px;
-    height: 60px;
-    font-size: 24px;
-
-    box-shadow: 2px 3px 6px #0000005d;
-    svg {
-      width: 28px;
-      height: 28px;
-    }
-  }
-
-  .btn-scroll-to-top {
-    width: 40px;
-    height: 40px;
-
-    opacity: 0.4;
-    svg {
-      width: 18px;
-      height: 18px;
-    }
-  }
-}
-
 // printable style
 // printable style
 @media print {
 @media print {
   body {
   body {

+ 3 - 1
packages/app/src/styles/theme/_apply-colors.scss

@@ -660,7 +660,9 @@ mark.rbt-highlight-text {
 
 
 .grw-fab {
 .grw-fab {
   .btn-create-page {
   .btn-create-page {
-    fill: hsl.contrast(var(--primary)) !important;
+    svg {
+      fill: hsl.contrast(var(--primary));
+    }
   }
   }
 
 
   .btn-scroll-to-top {
   .btn-scroll-to-top {

+ 1 - 3
packages/app/test/cypress/integration/10-install/10-install--install.spec.ts

@@ -53,8 +53,6 @@ describe('Install', () => {
     cy.getByTestid('grw-pagetree-item-container', { timeout: 20000 }).should('be.visible');
     cy.getByTestid('grw-pagetree-item-container', { timeout: 20000 }).should('be.visible');
 
 
     cy.waitUntilSkeletonDisappear();
     cy.waitUntilSkeletonDisappear();
-    cy.screenshot(`${ssPrefix}-installed-redirect-to-root-page`, {
-      blackout: ['[data-hide-in-vrt=true]']
-    });
+    cy.screenshot(`${ssPrefix}-installed-redirect-to-root-page`);
   });
   });
 });
 });

+ 2 - 2
packages/app/test/cypress/integration/20-basic-features/20-basic-features--access-to-page.spec.ts

@@ -29,8 +29,8 @@ context('Access to page', () => {
     // https://redmine.weseek.co.jp/issues/111384
     // https://redmine.weseek.co.jp/issues/111384
     // cy.get('.toc-link').should('be.visible');
     // cy.get('.toc-link').should('be.visible');
 
 
-    // hide fab // disable fab for sticky-events warning
-    // cy.getByTestid('grw-fab-container').invoke('attr', 'style', 'display: none');
+    // hide fab
+    cy.getByTestid('grw-fab-container').invoke('attr', 'style', 'display: none');
 
 
     // remove animation for screenshot
     // remove animation for screenshot
     // remove 'blink' class because ::after element cannot be operated
     // remove 'blink' class because ::after element cannot be operated

+ 2 - 2
packages/app/test/cypress/integration/21-basic-features-for-guest/21-basic-features-for-guest--access-to-page.spec.ts

@@ -14,8 +14,8 @@ context('Access to page by guest', () => {
     cy.visit('/Sandbox#Headers');
     cy.visit('/Sandbox#Headers');
     cy.waitUntilSkeletonDisappear();
     cy.waitUntilSkeletonDisappear();
 
 
-    // hide fab // disable fab for sticky-events warning
-    // cy.getByTestid('grw-fab-container').invoke('attr', 'style', 'display: none');
+    // hide fab
+    cy.getByTestid('grw-fab-container').invoke('attr', 'style', 'display: none');
 
 
     // remove animation for screenshot
     // remove animation for screenshot
     // remove 'blink' class because ::after element cannot be operated
     // remove 'blink' class because ::after element cannot be operated

+ 26 - 32
packages/app/test/cypress/integration/50-sidebar/50-sidebar--access-to-side-bar.spec.ts

@@ -1,3 +1,11 @@
+import { BlackoutGroup } from "../../support/blackout";
+
+// Blackout for recalculation of toc content hight
+const blackoutOverride = [
+  ...BlackoutGroup.BASIS,
+  ...BlackoutGroup.SIDE_CONTENTS,
+];
+
 describe('Access to sidebar', () => {
 describe('Access to sidebar', () => {
   const ssPrefix = 'access-to-sidebar-';
   const ssPrefix = 'access-to-sidebar-';
 
 
@@ -29,8 +37,7 @@ describe('Access to sidebar', () => {
           cy.waitUntilSkeletonDisappear();
           cy.waitUntilSkeletonDisappear();
           cy.screenshot(`${ssPrefix}1-sidebar-shown`, {
           cy.screenshot(`${ssPrefix}1-sidebar-shown`, {
             capture: 'viewport',
             capture: 'viewport',
-            // Blackout for recalculation of toc content hight
-            blackout: ['.grw-side-contents-container', '[data-hide-in-vrt=true]'],
+            blackout: blackoutOverride,
           });
           });
         });
         });
 
 
@@ -42,8 +49,7 @@ describe('Access to sidebar', () => {
           cy.waitUntilSkeletonDisappear();
           cy.waitUntilSkeletonDisappear();
           cy.screenshot(`${ssPrefix}2-sidebar-collapsed`, {
           cy.screenshot(`${ssPrefix}2-sidebar-collapsed`, {
             capture: 'viewport',
             capture: 'viewport',
-            // Blackout for recalculation of toc content hight
-            blackout: ['.grw-side-contents-container', '[data-hide-in-vrt=true]'],
+            blackout: blackoutOverride,
           });
           });
         });
         });
       });
       });
@@ -64,7 +70,7 @@ describe('Access to sidebar', () => {
             cy.getByTestid('grw-pagetree-item-container').should('be.visible');
             cy.getByTestid('grw-pagetree-item-container').should('be.visible');
 
 
             cy.waitUntilSkeletonDisappear();
             cy.waitUntilSkeletonDisappear();
-            cy.screenshot(`${ssPrefix}page-tree-1-access-to-page-tree`);
+            cy.screenshot(`${ssPrefix}page-tree-1-access-to-page-tree`, { blackout: blackoutOverride });
           });
           });
         });
         });
 
 
@@ -75,7 +81,7 @@ describe('Access to sidebar', () => {
             // hide page tree tiems
             // hide page tree tiems
             cy.get('.grw-pagetree-triangle-btn').first().click();
             cy.get('.grw-pagetree-triangle-btn').first().click();
 
 
-            cy.screenshot(`${ssPrefix}page-tree-2-hide-page-tree-items`);
+            cy.screenshot(`${ssPrefix}page-tree-2-hide-page-tree-items`, { blackout: blackoutOverride });
           });
           });
         });
         });
 
 
@@ -91,10 +97,7 @@ describe('Access to sidebar', () => {
             return cy.get('.dropdown-menu.show').then($elem => $elem.is(':visible'));
             return cy.get('.dropdown-menu.show').then($elem => $elem.is(':visible'));
           });
           });
 
 
-          cy.screenshot(`${ssPrefix}page-tree-3-before-click-button`, {
-            // Blackout for recalculation of toc content hight
-            blackout: ['.grw-side-contents-container', '[data-hide-in-vrt=true]'],
-          });
+          cy.screenshot(`${ssPrefix}page-tree-3-before-click-button`, { blackout: blackoutOverride });
 
 
           // click add remove bookmark btn
           // click add remove bookmark btn
           cy.getByTestid('page-item-control-menu').should('have.class', 'show')
           cy.getByTestid('page-item-control-menu').should('have.class', 'show')
@@ -112,10 +115,7 @@ describe('Access to sidebar', () => {
             return cy.get('.dropdown-menu.show').then($elem => $elem.is(':visible'));
             return cy.get('.dropdown-menu.show').then($elem => $elem.is(':visible'));
           });
           });
 
 
-          cy.screenshot(`${ssPrefix}page-tree-4-after-click-button`, {
-            // Blackout for recalculation of toc content hight
-            blackout: ['.grw-side-contents-container', '[data-hide-in-vrt=true]'],
-          });
+          cy.screenshot(`${ssPrefix}page-tree-4-after-click-button`, { blackout: blackoutOverride });
         });
         });
 
 
         it('Successfully show duplicate page modal', () => {
         it('Successfully show duplicate page modal', () => {
@@ -137,7 +137,7 @@ describe('Access to sidebar', () => {
           cy.getByTestid('page-duplicate-modal').should('be.visible').within(() => {
           cy.getByTestid('page-duplicate-modal').should('be.visible').within(() => {
             cy.get('.form-control').type('_test');
             cy.get('.form-control').type('_test');
 
 
-            cy.screenshot(`${ssPrefix}page-tree-5-duplicate-page-modal`);
+            cy.screenshot(`${ssPrefix}page-tree-5-duplicate-page-modal`, { blackout: blackoutOverride });
 
 
             cy.get('.modal-header > button').click();
             cy.get('.modal-header > button').click();
           });
           });
@@ -163,7 +163,7 @@ describe('Access to sidebar', () => {
             cy.getByTestid('closable-text-input').type('_newname');
             cy.getByTestid('closable-text-input').type('_newname');
           })
           })
 
 
-          cy.screenshot(`${ssPrefix}page-tree-6-rename-page`);
+          cy.screenshot(`${ssPrefix}page-tree-6-rename-page`, { blackout: blackoutOverride });
         });
         });
 
 
         it('Successfully show delete page modal', () => {
         it('Successfully show delete page modal', () => {
@@ -183,7 +183,7 @@ describe('Access to sidebar', () => {
           });
           });
 
 
           cy.getByTestid('page-delete-modal').should('be.visible').within(() => {
           cy.getByTestid('page-delete-modal').should('be.visible').within(() => {
-            cy.screenshot(`${ssPrefix}page-tree-7-delete-page-modal`);
+            cy.screenshot(`${ssPrefix}page-tree-7-delete-page-modal`, { blackout: blackoutOverride });
             cy.get('.modal-header > button').click();
             cy.get('.modal-header > button').click();
           });
           });
         });
         });
@@ -205,7 +205,7 @@ describe('Access to sidebar', () => {
             cy.get('.grw-sidebar-content-header > h3').find('a');
             cy.get('.grw-sidebar-content-header > h3').find('a');
 
 
             cy.waitUntilSkeletonDisappear();
             cy.waitUntilSkeletonDisappear();
-            cy.screenshot(`${ssPrefix}custom-sidebar-1-access-to-custom-sidebar`);
+            cy.screenshot(`${ssPrefix}custom-sidebar-1-access-to-custom-sidebar`, { blackout: blackoutOverride });
           });
           });
         });
         });
 
 
@@ -217,7 +217,7 @@ describe('Access to sidebar', () => {
           cy.get('.layout-root').should('have.class', 'editing');
           cy.get('.layout-root').should('have.class', 'editing');
           cy.get('.CodeMirror textarea').type(content, {force: true});
           cy.get('.CodeMirror textarea').type(content, {force: true});
 
 
-          cy.screenshot(`${ssPrefix}custom-sidebar-2-redirect-to-editor`);
+          cy.screenshot(`${ssPrefix}custom-sidebar-2-redirect-to-editor`, { blackout: blackoutOverride });
 
 
           cy.getByTestid('save-page-btn').click();
           cy.getByTestid('save-page-btn').click();
           cy.get('.layout-root').should('not.have.class', 'editing');
           cy.get('.layout-root').should('not.have.class', 'editing');
@@ -229,7 +229,7 @@ describe('Access to sidebar', () => {
             .should('have.class', 'active');
             .should('have.class', 'active');
 
 
           cy.waitUntilSkeletonDisappear();
           cy.waitUntilSkeletonDisappear();
-          cy.screenshot(`${ssPrefix}custom-sidebar-3-content-created`);
+          cy.screenshot(`${ssPrefix}custom-sidebar-3-content-created`, { blackout: blackoutOverride });
         });
         });
       });
       });
 
 
@@ -249,10 +249,7 @@ describe('Access to sidebar', () => {
           cy.get('.list-group-item').should('be.visible');
           cy.get('.list-group-item').should('be.visible');
 
 
           // The scope of the screenshot is not narrowed because the blackout is shifted
           // The scope of the screenshot is not narrowed because the blackout is shifted
-          cy.screenshot(`${ssPrefix}recent-changes-1-access-to-recent-changes`, {
-            // Blackout for recalculation of toc content hight
-            blackout: ['.grw-side-contents-container', '[data-hide-in-vrt=true]'],
-          });
+          cy.screenshot(`${ssPrefix}recent-changes-1-access-to-recent-changes`, { blackout: blackoutOverride });
         });
         });
 
 
         it('Successfully switch content size', () => {
         it('Successfully switch content size', () => {
@@ -262,10 +259,7 @@ describe('Access to sidebar', () => {
           });
           });
 
 
           // The scope of the screenshot is not narrowed because the blackout is shifted
           // The scope of the screenshot is not narrowed because the blackout is shifted
-          cy.screenshot(`${ssPrefix}recent-changes-2-switch-content-size`, {
-            // Blackout for recalculation of toc content hight
-            blackout: ['.grw-side-contents-container', '[data-hide-in-vrt=true]'],
-          });
+          cy.screenshot(`${ssPrefix}recent-changes-2-switch-content-size`, { blackout: blackoutOverride });
         });
         });
       });
       });
 
 
@@ -284,7 +278,7 @@ describe('Access to sidebar', () => {
           cy.getByTestid('grw-contextual-navigation-sub').within(() => {
           cy.getByTestid('grw-contextual-navigation-sub').within(() => {
             cy.getByTestid('grw-tags-list').should('be.visible');
             cy.getByTestid('grw-tags-list').should('be.visible');
 
 
-            cy.screenshot(`${ssPrefix}tags-1-access-to-tags`);
+            cy.screenshot(`${ssPrefix}tags-1-access-to-tags`, { blackout: blackoutOverride });
           });
           });
         });
         });
 
 
@@ -293,7 +287,7 @@ describe('Access to sidebar', () => {
           cy.collapseSidebar(true);
           cy.collapseSidebar(true);
           cy.getByTestid('grw-tags-list').should('be.visible');
           cy.getByTestid('grw-tags-list').should('be.visible');
 
 
-          cy.screenshot(`${ssPrefix}tags-2-click-all-tags-button`);
+          cy.screenshot(`${ssPrefix}tags-2-click-all-tags-button`, { blackout: blackoutOverride });
         });
         });
       });
       });
 
 
@@ -304,7 +298,7 @@ describe('Access to sidebar', () => {
       //   cy.get('.grw-sidebar-nav-secondary-container').within(() => {
       //   cy.get('.grw-sidebar-nav-secondary-container').within(() => {
       //     cy.get('a[href*="/me/drafts"]').click();
       //     cy.get('a[href*="/me/drafts"]').click();
       //   });
       //   });
-      //   cy.screenshot(`${ssPrefix}access-to-drafts-page`);
+      //   cy.screenshot(`${ssPrefix}access-to-drafts-page`, { blackout: blackoutOverride });
       // });
       // });
 
 
       describe('Test access to GROWI Docs page', () => {
       describe('Test access to GROWI Docs page', () => {
@@ -328,7 +322,7 @@ describe('Access to sidebar', () => {
           cy.get('.grw-page-path-hierarchical-link').should('be.visible');
           cy.get('.grw-page-path-hierarchical-link').should('be.visible');
           cy.get('.grw-custom-nav-tab').should('be.visible');
           cy.get('.grw-custom-nav-tab').should('be.visible');
 
 
-          cy.screenshot(`${ssPrefix}access-to-trash-page`);
+          cy.screenshot(`${ssPrefix}access-to-trash-page`, { blackout: blackoutOverride });
         });
         });
       });
       });
     });
     });

+ 10 - 4
packages/app/test/cypress/integration/50-sidebar/50-sidebar--switching-sidebar-mode.spec.ts

@@ -1,3 +1,11 @@
+import { BlackoutGroup } from "../../support/blackout";
+
+// Blackout for recalculation of toc content hight
+const blackoutOverride = [
+  ...BlackoutGroup.BASIS,
+  ...BlackoutGroup.SIDE_CONTENTS,
+];
+
 context('Switch sidebar mode', () => {
 context('Switch sidebar mode', () => {
   const ssPrefix = 'switch-sidebar-mode-';
   const ssPrefix = 'switch-sidebar-mode-';
 
 
@@ -28,15 +36,13 @@ context('Switch sidebar mode', () => {
     cy.get('[for="swSidebarMode"]').click({force: true});
     cy.get('[for="swSidebarMode"]').click({force: true});
     cy.get('.grw-sidebar-nav').should('not.be.visible');
     cy.get('.grw-sidebar-nav').should('not.be.visible');
     cy.screenshot(`${ssPrefix}-switch-sidebar-mode`, {
     cy.screenshot(`${ssPrefix}-switch-sidebar-mode`, {
-      // Blackout for recalculation of toc content hight
-      blackout: ['.grw-side-contents-container', '[data-hide-in-vrt=true]'],
+      blackout: blackoutOverride,
     });
     });
 
 
     cy.get('[for="swSidebarMode"]').click({force: true});
     cy.get('[for="swSidebarMode"]').click({force: true});
     cy.get('.grw-sidebar-nav').should('be.visible');
     cy.get('.grw-sidebar-nav').should('be.visible');
     cy.screenshot(`${ssPrefix}-switch-sidebar-mode-back`, {
     cy.screenshot(`${ssPrefix}-switch-sidebar-mode-back`, {
-      // Blackout for recalculation of toc content hight
-      blackout: ['.grw-side-contents-container','[data-hide-in-vrt=true]'],
+      blackout: blackoutOverride,
     });
     });
   });
   });
 
 

+ 2 - 2
packages/app/test/cypress/integration/60-home/60-home--home.spec.ts

@@ -46,8 +46,8 @@ context('Access User settings', () => {
     });
     });
     cy.visit('/me');
     cy.visit('/me');
     cy.collapseSidebar(true, true);
     cy.collapseSidebar(true, true);
-    // hide fab // disable fab for sticky-events warning
-    // cy.getByTestid('grw-fab-container').invoke('attr', 'style', 'display: none');
+    // hide fab
+    cy.getByTestid('grw-fab-container').invoke('attr', 'style', 'display: none');
   });
   });
 
 
   it('Access User information', () => {
   it('Access User information', () => {

+ 14 - 0
packages/app/test/cypress/support/blackout.ts

@@ -0,0 +1,14 @@
+export const BlackoutGroup = {
+  BASIS: [
+    '[data-vrt-blackout=true]',
+    '[data-vrt-blackout-hash=true]',
+    '[data-vrt-blackout-profile=true]',
+    '[data-vrt-blackout-datetime=true]',
+  ],
+  SIDEBAR_NAV: [
+    '[data-vrt-blackout-sidebar-nav=true]',
+  ],
+  SIDE_CONTENTS: [
+    '[data-vrt-blackout-side-contents=true]',
+  ],
+} as const;

+ 6 - 1
packages/app/test/cypress/support/screenshot.ts

@@ -1,4 +1,9 @@
+import { BlackoutGroup } from "./blackout";
+
 Cypress.Screenshot.defaults({
 Cypress.Screenshot.defaults({
-  blackout: ['[data-hide-in-vrt=true]'],
+  blackout: [
+    ...BlackoutGroup.BASIS,
+    ...BlackoutGroup.SIDEBAR_NAV,
+  ],
   capture: 'viewport',
   capture: 'viewport',
 })
 })

+ 1 - 1
packages/codemirror-textlint/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/codemirror-textlint",
   "name": "@growi/codemirror-textlint",
-  "version": "6.0.1-RC.0",
+  "version": "6.0.3-RC.0",
   "license": "MIT",
   "license": "MIT",
   "main": "dist/index.js",
   "main": "dist/index.js",
   "scripts": {
   "scripts": {

+ 1 - 1
packages/core/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/core",
   "name": "@growi/core",
-  "version": "6.0.1-RC.0",
+  "version": "6.0.3-RC.0",
   "description": "GROWI Core Libraries",
   "description": "GROWI Core Libraries",
   "license": "MIT",
   "license": "MIT",
   "keywords": [
   "keywords": [

+ 1 - 1
packages/hackmd/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/hackmd",
   "name": "@growi/hackmd",
-  "version": "6.0.1-RC.0",
+  "version": "6.0.3-RC.0",
   "description": "GROWI js and css files to use hackmd",
   "description": "GROWI js and css files to use hackmd",
   "license": "MIT",
   "license": "MIT",
   "main": "dist/index.js",
   "main": "dist/index.js",

+ 1 - 1
packages/preset-themes/package.json

@@ -1,7 +1,7 @@
 {
 {
   "name": "@growi/preset-themes",
   "name": "@growi/preset-themes",
   "description": "GROWI preset themes",
   "description": "GROWI preset themes",
-  "version": "6.0.1-RC.0",
+  "version": "6.0.3-RC.0",
   "license": "MIT",
   "license": "MIT",
   "main": "dist/libs/index.js",
   "main": "dist/libs/index.js",
   "files": [
   "files": [

+ 1 - 1
packages/remark-drawio/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/remark-drawio",
   "name": "@growi/remark-drawio",
-  "version": "6.0.1-RC.0",
+  "version": "6.0.3-RC.0",
   "description": "remark plugin to draw diagrams with draw.io (diagrams.net)",
   "description": "remark plugin to draw diagrams with draw.io (diagrams.net)",
   "license": "MIT",
   "license": "MIT",
   "keywords": [
   "keywords": [

+ 1 - 1
packages/remark-growi-directive/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/remark-growi-directive",
   "name": "@growi/remark-growi-directive",
-  "version": "6.0.1-RC.0",
+  "version": "6.0.3-RC.0",
   "description": "remark plugin to support GROWI plugin (forked from remark-directive@2.0.1)",
   "description": "remark plugin to support GROWI plugin (forked from remark-directive@2.0.1)",
   "license": "MIT",
   "license": "MIT",
   "keywords": [
   "keywords": [

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff