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

Merge branch 'master' into feat/ldap-group-sync

Futa Arai 2 лет назад
Родитель
Сommit
49633b4e64
56 измененных файлов с 480 добавлено и 220 удалено
  1. 1 1
      .github/ISSUE_TEMPLATE/config.yml
  2. 55 1
      CHANGELOG.md
  3. 2 2
      README.md
  4. 2 2
      README_JP.md
  5. 1 1
      SECURITY.md
  6. 1 1
      apps/app/package.json
  7. 5 1
      apps/app/public/static/locales/en_US/admin.json
  8. 1 0
      apps/app/public/static/locales/en_US/translation.json
  9. 7 2
      apps/app/public/static/locales/ja_JP/admin.json
  10. 1 0
      apps/app/public/static/locales/ja_JP/translation.json
  11. 5 1
      apps/app/public/static/locales/zh_CN/admin.json
  12. 1 0
      apps/app/public/static/locales/zh_CN/translation.json
  13. 1 1
      apps/app/resource/locales/en_US/welcome.md
  14. 1 1
      apps/app/resource/locales/ja_JP/welcome.md
  15. 1 1
      apps/app/resource/locales/zh_CN/welcome.md
  16. 21 2
      apps/app/src/client/services/AdminCustomizeContainer.js
  17. 58 0
      apps/app/src/client/services/search-operation.ts
  18. 4 0
      apps/app/src/components/Admin/Customize/Customize.jsx
  19. 0 28
      apps/app/src/components/Admin/Customize/CustomizeFunctionSetting.tsx
  20. 70 0
      apps/app/src/components/Admin/Customize/CustomizePresentationSetting.tsx
  21. 4 5
      apps/app/src/components/Navbar/GlobalSearch.tsx
  22. 10 1
      apps/app/src/components/Page/RenderTagLabels.tsx
  23. 7 0
      apps/app/src/components/Presentation/Slides.tsx
  24. 1 1
      apps/app/src/components/ReactMarkdownComponents/SlideViewer.tsx
  25. 15 40
      apps/app/src/components/SearchPage.tsx
  26. 32 20
      apps/app/src/components/SearchPage/SearchControl.tsx
  27. 4 1
      apps/app/src/components/Sidebar/CustomSidebar.tsx
  28. 10 7
      apps/app/src/components/Sidebar/RecentChanges.tsx
  29. 7 9
      apps/app/src/components/TagCloudBox.tsx
  30. 12 15
      apps/app/src/components/TagList.tsx
  31. 24 4
      apps/app/src/server/routes/apiv3/customize-setting.js
  32. 2 1
      apps/app/src/server/routes/apiv3/users.js
  33. 3 1
      apps/app/src/server/routes/attachment.js
  34. 13 4
      apps/app/src/stores/tag.tsx
  35. 2 1
      apps/app/test/cypress/e2e/20-basic-features/20-basic-features--sticky-features.cy.ts
  36. 1 1
      apps/slackbot-proxy/package.json
  37. 1 1
      apps/slackbot-proxy/src/views/top.ejs
  38. 1 1
      package.json
  39. 1 1
      packages/core/package.json
  40. 1 1
      packages/hackmd/package.json
  41. 8 6
      packages/presentation/package.json
  42. 0 2
      packages/presentation/src/components/GrowiSlides.tsx
  43. 0 1
      packages/presentation/src/components/MarpSlides.tsx
  44. 0 4
      packages/presentation/src/components/Slides.global.scss
  45. 35 0
      packages/presentation/src/components/Slides.module.scss
  46. 10 6
      packages/presentation/src/components/Slides.tsx
  47. 16 18
      packages/presentation/src/services/growi-marpit.ts
  48. 1 1
      packages/preset-templates/package.json
  49. 1 1
      packages/preset-themes/package.json
  50. 3 3
      packages/remark-attachment-refs/package.json
  51. 2 3
      packages/remark-drawio/package.json
  52. 1 1
      packages/remark-growi-directive/package.json
  53. 1 1
      packages/remark-lsx/package.json
  54. 1 1
      packages/slack/package.json
  55. 1 1
      packages/ui/package.json
  56. 11 11
      yarn.lock

+ 1 - 1
.github/ISSUE_TEMPLATE/config.yml

@@ -4,5 +4,5 @@ contact_links:
     url: https://github.com/weseek/growi/discussions
     url: https://github.com/weseek/growi/discussions
     about: If you have feature requests or suggestions, you can create a new discussion and consider it with the community.
     about: If you have feature requests or suggestions, you can create a new discussion and consider it with the community.
   - name: Questions
   - name: Questions
-    url: https://growi-slackin.weseek.co.jp/
+    url: https://communityinviter.com/apps/wsgrowi/invite/
     about: If you have questions, you can join our Slack team and talk about anything, anytime.
     about: If you have questions, you can join our Slack team and talk about anything, anytime.

+ 55 - 1
CHANGELOG.md

@@ -1,9 +1,63 @@
 # Changelog
 # Changelog
 
 
-## [Unreleased](https://github.com/weseek/growi/compare/v6.1.12...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v6.2.0...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.1.15](https://github.com/weseek/growi/compare/v6.1.14...v6.1.15) - 2023-09-11
+
+### 🚀 Improvement
+
+- imprv: Add CSP style-src for Safari and Content-Disposition of attachment (for v6.1.x) (#8057) @yuki-takei
+
+## [v6.1.14](https://github.com/weseek/growi/compare/v6.1.13...v6.1.14) - 2023-08-22
+
+### 🐛 Bug Fixes
+
+- fix: Add option to lightbox (6.1.x) (#8003) @yuki-takei
+
+## [v6.1.13](https://github.com/weseek/growi/compare/v6.1.12...v6.1.13) - 2023-08-17
+
+### 🐛 Bug Fixes
+
+- fix: Do not work img tag if use style property (#7988) @jam411
+- fix: "Searching..." label appearing unnecessarily (#7990) @yuki-takei
+
+## [v6.2.0](https://github.com/weseek/growi/compare/v6.1.12...v6.2.0) - 2023-09-14
+
+### 💎 Features
+
+- feat: Presentation preview and support Marp  (#8029) @reiji-h
+
+### 🚀 Improvement
+
+- imprv: Able to customize users homepage deletion (#7921) @yuki-takei
+- imprv: Search behavior (#8069) @yuki-takei
+- imprv: Add CSP style-src for Safari and Content-Disposition of attachment (#8049) @ykanematsu
+- imprv: Correct update message (#8040) @reiji-h
+- imprv: Add installed date to questionnaire answer (#7971) @TatsuyaIse
+- imprv: Show modal when you delete plugin (#7875) @soumaeda
+- imprv: i18n resetting password mail body (#8058) @meiri-k
+- imprv: Create Japanese ejs files (#7957) @meiri-k
+- imprv: Clean up old toastr (#7949) @jam411
+- imprv: Persist the installed date in the Config collection (#7936) @TatsuyaIse
+
+### 🐛 Bug Fixes
+
+- fix: Pages can be created under a non-existent user page (#7974) @miya
+- fix: Pages can be created under a non-existent user page (During attachment upload) (#8001) @miya
+- fix: Type safe implementation for objects imported from ElasticsearchClient (#7862) @miya
+- fix: Consider an empty page when renaming and duplicating (#7979) @yuki-takei
+- fix: Remove redundant toastSuccess for pasted attachments (#8044) @jam411
+- fix: Fixing swagger for tag update api (#8010) @miya
+- fix: Modification of links in the docs (#8004) @miya
+
+### 🧰 Maintenance
+
+- support: Omit core-js v2 (#7944) @yuki-takei
+- support: Improve build settings (#7919) @yuki-takei
+- support: Url to join to the slack team (#8073) @WNomunomu
+
 ## [v6.1.12](https://github.com/weseek/growi/compare/v6.1.11...v6.1.12) - 2023-08-14
 ## [v6.1.12](https://github.com/weseek/growi/compare/v6.1.11...v6.1.12) - 2023-08-14
 
 
 ### 🐛 Bug Fixes
 ### 🐛 Bug Fixes

+ 2 - 2
README.md

@@ -7,7 +7,7 @@
 </p>
 </p>
 <p align="center">
 <p align="center">
   <a href="https://github.com/weseek/growi/releases/latest"><img src="https://img.shields.io/github/release/weseek/growi.svg"></a>
   <a href="https://github.com/weseek/growi/releases/latest"><img src="https://img.shields.io/github/release/weseek/growi.svg"></a>
-  <a href="https://growi-slackin.weseek.co.jp/"><img src="https://growi-slackin.weseek.co.jp/badge.svg"></a>
+  <a href="https://communityinviter.com/apps/wsgrowi/invite/">join our Slack team</a>
 </p>
 </p>
 
 
 <p align="center">
 <p align="center">
@@ -132,7 +132,7 @@ You can write issues and PRs in English or Japanese.
 
 
 ## Discussion
 ## Discussion
 
 
-If you have questions or suggestions, you can [join our Slack team](https://growi-slackin.weseek.co.jp/) and talk about anything, anytime.
+If you have questions or suggestions, you can [join our Slack team](https://communityinviter.com/apps/wsgrowi/invite/) and talk about anything, anytime.
 
 
 # License
 # License
 
 

+ 2 - 2
README_JP.md

@@ -6,7 +6,7 @@
   </p>
   </p>
   <p align="center">
   <p align="center">
     <a href="https://github.com/weseek/growi/releases/latest"><img src="https://img.shields.io/github/release/weseek/growi.svg"></a>
     <a href="https://github.com/weseek/growi/releases/latest"><img src="https://img.shields.io/github/release/weseek/growi.svg"></a>
-    <a href="https://growi-slackin.weseek.co.jp/"><img src="https://growi-slackin.weseek.co.jp/badge.svg"></a>
+    <a href="https://communityinviter.com/apps/wsgrowi/invite/">join our Slack team</a>
   </p>
   </p>
 
 
 <p align="center">
 <p align="center">
@@ -129,7 +129,7 @@ Issue と Pull requests の作成は英語・日本語どちらでも受け付
 
 
 ## GROWI について話し合いましょう!
 ## GROWI について話し合いましょう!
 
 
-質問や提案があれば、私たちの [Slack team](https://growi-slackin.weseek.co.jp/) にぜひご参加ください。
+質問や提案があれば、私たちの [Slack team](https://communityinviter.com/apps/wsgrowi/invite/) にぜひご参加ください。
 いつでも、どこでも GROWI について議論しましょう!
 いつでも、どこでも GROWI について議論しましょう!
 
 
 # ライセンス
 # ライセンス

+ 1 - 1
SECURITY.md

@@ -13,7 +13,7 @@
 
 
 If you believe you have found a security vulnerability in any GROWI related repository, please report it to us using one of the methods described below.
 If you believe you have found a security vulnerability in any GROWI related repository, please report it to us using one of the methods described below.
 
 
-  * [Join our Slack team](https://growi-slackin.weseek.co.jp/) and send DM to `@yuki` who is the lead developer
+  * [Join our Slack team](https://communityinviter.com/apps/wsgrowi/invite/) and send DM to `@yuki` who is the lead developer
   * Report to JPCERT/CC[^jpcertcc]
   * Report to JPCERT/CC[^jpcertcc]
     * [[PDF] JPCERT/CC Vulnerability Coordination and Disclosure Policy](https://www.jpcert.or.jp/english/vh/vul-coordination-disclosure-policy_2019.pdf)
     * [[PDF] JPCERT/CC Vulnerability Coordination and Disclosure Policy](https://www.jpcert.or.jp/english/vh/vul-coordination-disclosure-policy_2019.pdf)
 
 

+ 1 - 1
apps/app/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/app",
   "name": "@growi/app",
-  "version": "6.2.0-RC.0",
+  "version": "6.2.1-RC.0",
   "license": "MIT",
   "license": "MIT",
   "scripts": {
   "scripts": {
     "//// for production": "",
     "//// for production": "",

+ 5 - 1
apps/app/public/static/locales/en_US/admin.json

@@ -480,7 +480,10 @@
       "show_all_reply_comments": "Show all reply comments",
       "show_all_reply_comments": "Show all reply comments",
       "show_all_reply_comments_desc": "When the setting value is off, comments other than the latest two are omitted.",
       "show_all_reply_comments_desc": "When the setting value is off, comments other than the latest two are omitted.",
       "select_search_scope_children_as_default": "Select 'Only children of this tree' as default value of search range",
       "select_search_scope_children_as_default": "Select 'Only children of this tree' as default value of search range",
-      "select_search_scope_children_as_default_desc": "When the setting value is off, 'All pages' is used as default value of search range.",
+      "select_search_scope_children_as_default_desc": "When the setting value is off, 'All pages' is used as default value of search range."
+    },
+      "presentation": "Presentation",
+    "presentation_options": {
       "enable_marp": "Enable Marp ",
       "enable_marp": "Enable Marp ",
       "enable_marp_desc": "Marp can be used in presentation preview. This option may make you vulnerable to XSS.",
       "enable_marp_desc": "Marp can be used in presentation preview. This option may make you vulnerable to XSS.",
       "marp_official_site": "The Marp Official Site",
       "marp_official_site": "The Marp Official Site",
@@ -499,6 +502,7 @@
     "write_css": "You can write CSS that is applied to whole system.",
     "write_css": "You can write CSS that is applied to whole system.",
     "ctrl_space": "Ctrl+Space to autocomplete",
     "ctrl_space": "Ctrl+Space to autocomplete",
     "custom_script": "Custom script",
     "custom_script": "Custom script",
+    "custom_presentation": "Custom presentation",
     "write_java": "You can write Javascript that is applied to whole system.",
     "write_java": "You can write Javascript that is applied to whole system.",
     "reflect_change": "You need to reload the page to reflect the change.",
     "reflect_change": "You need to reload the page to reflect the change.",
     "custom_logo" : "Custom Logo",
     "custom_logo" : "Custom Logo",

+ 1 - 0
apps/app/public/static/locales/en_US/translation.json

@@ -66,6 +66,7 @@
   "No users have liked this yet.": "No users have liked this yet.",
   "No users have liked this yet.": "No users have liked this yet.",
   "No users have bookmarked yet": "No users have bookmarked yet",
   "No users have bookmarked yet": "No users have bookmarked yet",
   "Create Archive Page": "Create Archive Page",
   "Create Archive Page": "Create Archive Page",
+  "Create Sidebar Page": "Create <strong>/Sidebar</strong> Page",
   "File type": "File type",
   "File type": "File type",
   "Target page": "Target page",
   "Target page": "Target page",
   "Include Attachment File": "Include Attachment File",
   "Include Attachment File": "Include Attachment File",

+ 7 - 2
apps/app/public/static/locales/ja_JP/admin.json

@@ -488,7 +488,11 @@
       "show_all_reply_comments": "返信コメントを全て表示する",
       "show_all_reply_comments": "返信コメントを全て表示する",
       "show_all_reply_comments_desc": "OFFの場合、最新2件のコメント以外が省略されます。",
       "show_all_reply_comments_desc": "OFFの場合、最新2件のコメント以外が省略されます。",
       "select_search_scope_children_as_default": "検索範囲のデフォルト設定を「この階層下の子ページ」にする",
       "select_search_scope_children_as_default": "検索範囲のデフォルト設定を「この階層下の子ページ」にする",
-      "select_search_scope_children_as_default_desc": "OFFの場合、検索範囲のデフォルト設定は「全てのページ」になります。",
+      "select_search_scope_children_as_default_desc": "OFFの場合、検索範囲のデフォルト設定は「全てのページ」になります。"
+
+    },
+    "presentation":"プレゼンテーション",
+    "presentation_options":{
       "enable_marp": "Marp を有効化する",
       "enable_marp": "Marp を有効化する",
       "enable_marp_desc": "プレゼンテーション表示に Marp を利用できるようになります。ただし、XSS に対して脆弱になる恐れがあります。",
       "enable_marp_desc": "プレゼンテーション表示に Marp を利用できるようになります。ただし、XSS に対して脆弱になる恐れがあります。",
       "marp_official_site": "参考:Marp 公式サイト",
       "marp_official_site": "参考:Marp 公式サイト",
@@ -507,6 +511,7 @@
     "write_css": " システム全体に適用されるCSSを記述できます。",
     "write_css": " システム全体に適用されるCSSを記述できます。",
     "ctrl_space": "Ctrl+Space でコード補完",
     "ctrl_space": "Ctrl+Space でコード補完",
     "custom_script": "カスタムスクリプト",
     "custom_script": "カスタムスクリプト",
+    "custom_presentation":"プレゼンテーション",
     "write_java": "システム全体に適用されるJavaScriptを記述できます。",
     "write_java": "システム全体に適用されるJavaScriptを記述できます。",
     "reflect_change": "変更の反映はページの更新が必要です。",
     "reflect_change": "変更の反映はページの更新が必要です。",
     "custom_logo": "カスタムロゴ",
     "custom_logo": "カスタムロゴ",
@@ -515,7 +520,7 @@
     "current_logo": "現在のロゴ",
     "current_logo": "現在のロゴ",
     "upload_new_logo": "新しいロゴをアップロードする",
     "upload_new_logo": "新しいロゴをアップロードする",
     "delete_logo": "ロゴを削除"
     "delete_logo": "ロゴを削除"
-  },
+   },
   "importer_management": {
   "importer_management": {
     "import_data": "データインポート",
     "import_data": "データインポート",
     "article": "記事",
     "article": "記事",

+ 1 - 0
apps/app/public/static/locales/ja_JP/translation.json

@@ -62,6 +62,7 @@
   "No users have liked this yet": "いいねをしているユーザーはいません",
   "No users have liked this yet": "いいねをしているユーザーはいません",
   "No users have bookmarked yet": "ブックマークしているユーザーはいません",
   "No users have bookmarked yet": "ブックマークしているユーザーはいません",
   "Create Archive Page": "アーカイブページの作成",
   "Create Archive Page": "アーカイブページの作成",
+  "Create Sidebar Page": "<strong>/Sidebar</strong> ページを作成する",
   "Target page": "対象ページ",
   "Target page": "対象ページ",
   "File type": "ファイル形式",
   "File type": "ファイル形式",
   "Include Attachment File": "添付ファイルも含める",
   "Include Attachment File": "添付ファイルも含める",

+ 5 - 1
apps/app/public/static/locales/zh_CN/admin.json

@@ -488,7 +488,10 @@
       "show_all_reply_comments": "显示所有回复评论",
       "show_all_reply_comments": "显示所有回复评论",
       "show_all_reply_comments_desc": "当设置值为“关”时,将忽略最近两个之外的注释。",
       "show_all_reply_comments_desc": "当设置值为“关”时,将忽略最近两个之外的注释。",
       "select_search_scope_children_as_default": "选择“当前分支以下内容”, 作为搜索范围的默认值",
       "select_search_scope_children_as_default": "选择“当前分支以下内容”, 作为搜索范围的默认值",
-      "select_search_scope_children_as_default_desc": "当设置值为“关”时,“所有页面”被作为搜索范围的默认值。",
+      "select_search_scope_children_as_default_desc": "当设置值为“关”时,“所有页面”被作为搜索范围的默认值。"
+    },
+      "presentation": "表达",
+      "presentation_options": {
       "enable_marp": "启用 Marp",
       "enable_marp": "启用 Marp",
       "enable_marp_desc": "Marp 可在演示视图中使用。该选项可能会使您受到 XSS 的攻击。",
       "enable_marp_desc": "Marp 可在演示视图中使用。该选项可能会使您受到 XSS 的攻击。",
       "marp_official_site": "参考资料:Marp 官方网站",
       "marp_official_site": "参考资料:Marp 官方网站",
@@ -507,6 +510,7 @@
     "write_css": "您可以编写应用于整个系统的CSS。",
     "write_css": "您可以编写应用于整个系统的CSS。",
     "ctrl_space": "Ctrl+Space 自动完成",
     "ctrl_space": "Ctrl+Space 自动完成",
     "custom_script": "定制纸条",
     "custom_script": "定制纸条",
+    "custom_presentation":"表达",
     "write_java": "您可以编写应用于整个系统的Javascript。",
     "write_java": "您可以编写应用于整个系统的Javascript。",
     "reflect_change": "您需要重新加载页面以反映更改。",
     "reflect_change": "您需要重新加载页面以反映更改。",
     "custom_logo": "自定义徽标",
     "custom_logo": "自定义徽标",

+ 1 - 0
apps/app/public/static/locales/zh_CN/translation.json

@@ -62,6 +62,7 @@
   "No users have liked this yet": "还没有用户喜欢这个",
   "No users have liked this yet": "还没有用户喜欢这个",
   "No users have bookmarked yet": "还没有用户加入书签",
   "No users have bookmarked yet": "还没有用户加入书签",
   "Create Archive Page": "创建归档页",
   "Create Archive Page": "创建归档页",
+  "Create Sidebar Page": "创建 <strong>/Sidebar</strong> 页面",
   "File type": "文件类型",
   "File type": "文件类型",
   "Target page": "目标页面",
   "Target page": "目标页面",
   "Include Attachment File": "包含附件",
   "Include Attachment File": "包含附件",

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

@@ -58,7 +58,7 @@ We can display the content list using a table and `$lsx`.
 
 
 # Slack
 # Slack
 
 
-<a href="https://growi-slackin.weseek.co.jp/"><img src="https://growi-slackin.weseek.co.jp/badge.svg"></a>
+<a href="https://communityinviter.com/apps/wsgrowi/invite/">join our Slack team</a>
 
 
 We welcome newcomers joining our slack channel to help improve GROWI.
 We welcome newcomers joining our slack channel to help improve GROWI.
 In addition to discussing development, we are also happy to answer your questions when you join.
 In addition to discussing development, we are also happy to answer your questions when you join.

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

@@ -54,7 +54,7 @@ GROWI は個人・法人向けの Wiki | ナレッジベースツールです。
 
 
 # Slack
 # Slack
 
 
-<a href="https://growi-slackin.weseek.co.jp/"><img src="https://growi-slackin.weseek.co.jp/badge.svg"></a>
+<a href="https://communityinviter.com/apps/wsgrowi/invite/">join our Slack team</a>
 
 
 GROWI をより良いものにするために、是非 Slack に参加してください。  
 GROWI をより良いものにするために、是非 Slack に参加してください。  
 開発に関する議論を行っている他、導入時の質問等も受け付けています。
 開発に関する議論を行っている他、導入時の質問等も受け付けています。

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

@@ -58,7 +58,7 @@ GROWI是一个针对个人和公司的Wiki - 一个知识库工具。
 
 
 # Slack
 # Slack
 
 
-<a href="https://growi-slackin.weseek.co.jp/"><img src="https://growi-slackin.weseek.co.jp/badge.svg"></a>
+<a href="https://communityinviter.com/apps/wsgrowi/invite/">join our Slack team</a>
 
 
 我们欢迎新人加入我们的slack频道,帮助改善Growi。
 我们欢迎新人加入我们的slack频道,帮助改善Growi。
 除了讨论发展问题,我们也很乐意在你加入时回答你的问题。
 除了讨论发展问题,我们也很乐意在你加入时回答你的问题。

+ 21 - 2
apps/app/src/client/services/AdminCustomizeContainer.js

@@ -1,3 +1,4 @@
+/* eslint-disable lines-between-class-members */
 import { isServer } from '@growi/core/dist/utils';
 import { isServer } from '@growi/core/dist/utils';
 import { Container } from 'unstated';
 import { Container } from 'unstated';
 
 
@@ -154,7 +155,7 @@ export default class AdminCustomizeContainer extends Container {
   /**
   /**
    * Switch isEnabledMarp
    * Switch isEnabledMarp
    */
    */
-  switchIsEnabledMarp() {
+  switchIsEnabledMarp(inputValue) {
     this.setState({ isEnabledMarp: !this.state.isEnabledMarp });
     this.setState({ isEnabledMarp: !this.state.isEnabledMarp });
   }
   }
 
 
@@ -203,7 +204,6 @@ export default class AdminCustomizeContainer extends Container {
         isEnabledStaleNotification: this.state.isEnabledStaleNotification,
         isEnabledStaleNotification: this.state.isEnabledStaleNotification,
         isAllReplyShown: this.state.isAllReplyShown,
         isAllReplyShown: this.state.isAllReplyShown,
         isSearchScopeChildrenAsDefault: this.state.isSearchScopeChildrenAsDefault,
         isSearchScopeChildrenAsDefault: this.state.isSearchScopeChildrenAsDefault,
-        isEnabledMarp: this.state.isEnabledMarp,
       });
       });
       const { customizedParams } = response.data;
       const { customizedParams } = response.data;
       this.setState({
       this.setState({
@@ -216,6 +216,25 @@ export default class AdminCustomizeContainer extends Container {
         isEnabledStaleNotification: customizedParams.isEnabledStaleNotification,
         isEnabledStaleNotification: customizedParams.isEnabledStaleNotification,
         isAllReplyShown: customizedParams.isAllReplyShown,
         isAllReplyShown: customizedParams.isAllReplyShown,
         isSearchScopeChildrenAsDefault: customizedParams.isSearchScopeChildrenAsDefault,
         isSearchScopeChildrenAsDefault: customizedParams.isSearchScopeChildrenAsDefault,
+      });
+    }
+    catch (err) {
+      logger.error(err);
+      throw new Error('Failed to update data');
+    }
+  }
+  /**
+   * Update presentation
+   * @memberOf AdminCustomizeContainer
+   */
+  async updateCustomizePresentation() {
+    try {
+      const response = await apiv3Put('/customize-setting/presentation', {
+        isEnabledMarp: this.state.isEnabledMarp,
+      });
+
+      const { customizedParams } = response.data;
+      this.setState({
         isEnabledMarp: customizedParams.isEnabledMarp,
         isEnabledMarp: customizedParams.isEnabledMarp,
       });
       });
     }
     }

+ 58 - 0
apps/app/src/client/services/search-operation.ts

@@ -0,0 +1,58 @@
+import { useCallback, useEffect, useRef } from 'react';
+
+import { type SWRResponseWithUtils, withUtils } from '@growi/core/dist/swr';
+import { useRouter } from 'next/router';
+import useSWRImmutable from 'swr/immutable';
+
+
+type UseKeywordManagerUtils = {
+  pushState: (newKeyword: string) => void,
+}
+
+export const useKeywordManager = (): SWRResponseWithUtils<UseKeywordManagerUtils, string> => {
+  // routerRef solve the problem of infinite redrawing that occurs with routers
+  const router = useRouter();
+  const routerRef = useRef(router);
+
+  // parse URL Query
+  const queries = router.query.q;
+  const initialKeyword = (Array.isArray(queries) ? queries.join(' ') : queries) ?? '';
+
+  const swrResponse = useSWRImmutable<string>('searchKeyword', null, {
+    fallbackData: initialKeyword,
+  });
+
+  const { mutate } = swrResponse;
+  const pushState = useCallback((newKeyword: string) => {
+    mutate((prevKeyword) => {
+      if (prevKeyword !== newKeyword) {
+        const newUrl = new URL('/_search', 'http://example.com');
+        newUrl.searchParams.append('q', newKeyword);
+        routerRef.current.push(`${newUrl.pathname}${newUrl.search}`, '');
+      }
+
+      return newKeyword;
+    });
+  }, [mutate]);
+
+  // detect search keyword from the query of URL
+  useEffect(() => {
+    mutate(initialKeyword);
+  }, [mutate, initialKeyword]);
+
+  // browser back and forward
+  useEffect(() => {
+    routerRef.current.beforePopState(({ url }) => {
+      const newUrl = new URL(url, 'https://exmple.com');
+      const newKeyword = newUrl.searchParams.get('q');
+      if (newKeyword != null) {
+        mutate(newKeyword);
+      }
+      return true;
+    });
+  }, [mutate]);
+
+  return withUtils(swrResponse, {
+    pushState,
+  });
+};

+ 4 - 0
apps/app/src/components/Admin/Customize/Customize.jsx

@@ -15,6 +15,7 @@ import CustomizeFunctionSetting from './CustomizeFunctionSetting';
 import CustomizeLayoutSetting from './CustomizeLayoutSetting';
 import CustomizeLayoutSetting from './CustomizeLayoutSetting';
 import CustomizeLogoSetting from './CustomizeLogoSetting';
 import CustomizeLogoSetting from './CustomizeLogoSetting';
 import CustomizeNoscriptSetting from './CustomizeNoscriptSetting';
 import CustomizeNoscriptSetting from './CustomizeNoscriptSetting';
+import CustomizePresentationSetting from './CustomizePresentationSetting';
 import CustomizeScriptSetting from './CustomizeScriptSetting';
 import CustomizeScriptSetting from './CustomizeScriptSetting';
 import CustomizeSidebarSetting from './CustomizeSidebarSetting';
 import CustomizeSidebarSetting from './CustomizeSidebarSetting';
 import CustomizeThemeSetting from './CustomizeThemeSetting';
 import CustomizeThemeSetting from './CustomizeThemeSetting';
@@ -58,6 +59,9 @@ function Customize(props) {
       <div className="mb-5">
       <div className="mb-5">
         <CustomizeFunctionSetting />
         <CustomizeFunctionSetting />
       </div>
       </div>
+      <div className="mb-5">
+        <CustomizePresentationSetting />
+      </div>
       <div className="mb-5">
       <div className="mb-5">
         <CustomizeTitle />
         <CustomizeTitle />
       </div>
       </div>

+ 0 - 28
apps/app/src/components/Admin/Customize/CustomizeFunctionSetting.tsx

@@ -133,34 +133,6 @@ const CustomizeFunctionSetting = (props: Props): JSX.Element => {
             </div>
             </div>
           </div>
           </div>
 
 
-          <div className="form-group row">
-            <div className="offset-md-3 col-md-6 text-left">
-              <CustomizeFunctionOption
-                optionId="isEnabledMarp"
-                label={t('admin:customize_settings.function_options.enable_marp')}
-                isChecked={adminCustomizeContainer.state.isEnabledMarp || false}
-                onChecked={() => { adminCustomizeContainer.switchIsEnabledMarp() }}
-              >
-                <p className="form-text text-muted">
-                  {t('admin:customize_settings.function_options.enable_marp_desc')}
-                  <br></br>
-                  <a
-                    href={`${t('admin:customize_settings.function_options.marp_official_site_link')}`}
-                    target="_blank"
-                    rel="noopener noreferrer"
-                  >{`${t('admin:customize_settings.function_options.marp_official_site')}`}
-                  </a>
-                  <br></br>
-                  <a
-                    href={`${t('admin:customize_settings.function_options.marp_in_gorwi_link')}`}
-                    target="_blank"
-                    rel="noopener noreferrer"
-                  >{`${t('admin:customize_settings.function_options.marp_in_growi')}`}
-                  </a>
-                </p>
-              </CustomizeFunctionOption>
-            </div>
-          </div>
 
 
           <AdminUpdateButtonRow onClick={onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
           <AdminUpdateButtonRow onClick={onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
         </div>
         </div>

+ 70 - 0
apps/app/src/components/Admin/Customize/CustomizePresentationSetting.tsx

@@ -0,0 +1,70 @@
+import React, { useCallback } from 'react';
+
+import { useTranslation } from 'next-i18next';
+
+import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
+import { toastSuccess, toastError } from '~/client/util/toastr';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
+import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
+
+import CustomizePresentationOption from './CustomizeFunctionOption';
+
+type Props = {
+  adminCustomizeContainer: AdminCustomizeContainer
+}
+
+const CustomizePresentationSetting = (props: Props): JSX.Element => {
+  const { adminCustomizeContainer } = props;
+
+  console.log(adminCustomizeContainer);
+
+  const { t } = useTranslation();
+  const onClickSubmit = useCallback(async() => {
+    try {
+      await adminCustomizeContainer.updateCustomizePresentation();
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.presentation'), ns: 'commons' }));
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }, [adminCustomizeContainer]);
+  return (
+    <React.Fragment>
+      <h2 className="admin-setting-header">{t('admin:customize_settings.custom_presentation')}</h2>
+      <div className="form-group row">
+        <div className="offset-md-3 col-md-6 text-left">
+          <CustomizePresentationOption
+            optionId="isEnabledMarp"
+            label={t('admin:customize_settings.presentation_options.enable_marp')}
+            isChecked={adminCustomizeContainer?.state.isEnabledMarp || false}
+            onChecked={() => { adminCustomizeContainer.switchIsEnabledMarp() }}
+          >
+            <p className="form-text text-muted">
+              {t('admin:customize_settings.presentation_options.enable_marp_desc')}
+              <br></br>
+              <a
+                href={`${t('admin:customize_settings.presentation_options.marp_official_site_link')}`}
+                target="_blank"
+                rel="noopener noreferrer"
+              >{`${t('admin:customize_settings.presentation_options.marp_official_site')}`}
+              </a>
+              <br></br>
+              <a
+                href={`${t('admin:customize_settings.presentation_options.marp_in_gorwi_link')}`}
+                target="_blank"
+                rel="noopener noreferrer"
+              >{`${t('admin:customize_settings.presenattion_options.marp_in_growi')}`}
+              </a>
+            </p>
+          </CustomizePresentationOption>
+        </div>
+      </div>
+
+      <AdminUpdateButtonRow onClick={onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
+    </React.Fragment>
+  );
+};
+const CustomizePresentationSettingWrapper = withUnstatedContainers(CustomizePresentationSetting, [AdminCustomizeContainer]);
+
+export default CustomizePresentationSettingWrapper;

+ 4 - 5
apps/app/src/components/Navbar/GlobalSearch.tsx

@@ -9,6 +9,7 @@ import { useTranslation } from 'next-i18next';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
 
 
 import { IFocusable } from '~/client/interfaces/focusable';
 import { IFocusable } from '~/client/interfaces/focusable';
+import { useKeywordManager } from '~/client/services/search-operation';
 import { IPageWithSearchMeta } from '~/interfaces/search';
 import { IPageWithSearchMeta } from '~/interfaces/search';
 import {
 import {
   useIsSearchScopeChildrenAsDefault, useIsSearchServiceReachable,
   useIsSearchScopeChildrenAsDefault, useIsSearchServiceReachable,
@@ -46,6 +47,8 @@ export const GlobalSearch = (props: GlobalSearchProps): JSX.Element => {
   const [isScopeChildren, setScopeChildren] = useState<boolean|undefined>(isSearchScopeChildrenAsDefault ?? false);
   const [isScopeChildren, setScopeChildren] = useState<boolean|undefined>(isSearchScopeChildrenAsDefault ?? false);
   const [isFocused, setFocused] = useState<boolean>(false);
   const [isFocused, setFocused] = useState<boolean>(false);
 
 
+  const { pushState } = useKeywordManager();
+
   useEffect(() => {
   useEffect(() => {
     setScopeChildren(isSearchScopeChildrenAsDefault);
     setScopeChildren(isSearchScopeChildrenAsDefault);
   }, [isSearchScopeChildrenAsDefault]);
   }, [isSearchScopeChildrenAsDefault]);
@@ -63,17 +66,13 @@ export const GlobalSearch = (props: GlobalSearchProps): JSX.Element => {
   }, [returnPathForURL, router]);
   }, [returnPathForURL, router]);
 
 
   const search = useCallback(() => {
   const search = useCallback(() => {
-    const url = new URL(window.location.href);
-    url.pathname = '/_search';
-
     // construct search query
     // construct search query
     let q = text;
     let q = text;
     if (isScopeChildren) {
     if (isScopeChildren) {
       q += ` prefix:${currentPagePath ?? window.location.pathname}`;
       q += ` prefix:${currentPagePath ?? window.location.pathname}`;
     }
     }
-    url.searchParams.append('q', q);
 
 
-    router.push(url.href);
+    pushState(q);
   }, [currentPagePath, isScopeChildren, router, text]);
   }, [currentPagePath, isScopeChildren, router, text]);
 
 
   const scopeLabel = isScopeChildren
   const scopeLabel = isScopeChildren

+ 10 - 1
apps/app/src/components/Page/RenderTagLabels.tsx

@@ -2,6 +2,8 @@ import React from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
+import { useKeywordManager } from '~/client/services/search-operation';
+
 import { NotAvailableForGuest } from '../NotAvailableForGuest';
 import { NotAvailableForGuest } from '../NotAvailableForGuest';
 import { NotAvailableForReadOnlyUser } from '../NotAvailableForReadOnlyUser';
 import { NotAvailableForReadOnlyUser } from '../NotAvailableForReadOnlyUser';
 
 
@@ -15,6 +17,8 @@ const RenderTagLabels = React.memo((props: RenderTagLabelsProps) => {
   const { tags, isTagLabelsDisabled, openEditorModal } = props;
   const { tags, isTagLabelsDisabled, openEditorModal } = props;
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
+  const { pushState } = useKeywordManager();
+
   function openEditorHandler() {
   function openEditorHandler() {
     if (openEditorModal == null) {
     if (openEditorModal == null) {
       return;
       return;
@@ -28,7 +32,12 @@ const RenderTagLabels = React.memo((props: RenderTagLabelsProps) => {
     <>
     <>
       {tags.map((tag) => {
       {tags.map((tag) => {
         return (
         return (
-          <a key={tag} href={`/_search?q=tag:${tag}`} className="grw-tag-label badge badge-secondary mr-2">
+          <a
+            key={tag}
+            type="button"
+            className="grw-tag-label badge badge-secondary mr-2"
+            onClick={() => pushState(`tag:${tag}`)}
+          >
             {tag}
             {tag}
           </a>
           </a>
         );
         );

+ 7 - 0
apps/app/src/components/Presentation/Slides.tsx

@@ -0,0 +1,7 @@
+import { Slides as SlidesSubstance, type SlidesProps } from '@growi/presentation';
+
+import '@growi/presentation/dist/style.css';
+
+export const Slides = (props: SlidesProps): JSX.Element => {
+  return <SlidesSubstance {...props} />;
+};

+ 1 - 1
apps/app/src/components/ReactMarkdownComponents/SlideViewer.tsx

@@ -6,7 +6,7 @@ import { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
 import { usePresentationViewOptions } from '~/stores/slide-viewer-renderer';
 import { usePresentationViewOptions } from '~/stores/slide-viewer-renderer';
 
 
 
 
-const Slides = dynamic(() => import('@growi/presentation').then(mod => mod.Slides), { ssr: false });
+const Slides = dynamic(() => import('../Presentation/Slides').then(mod => mod.Slides), { ssr: false });
 
 
 type SlideViewerProps = {
 type SlideViewerProps = {
   marp: string | undefined,
   marp: string | undefined,

+ 15 - 40
apps/app/src/components/SearchPage.tsx

@@ -1,23 +1,21 @@
 import React, {
 import React, {
-  useCallback, useEffect, useMemo, useRef, useState,
+  useCallback, useMemo, useRef, useState,
 } from 'react';
 } from 'react';
 
 
-
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
-import { useRouter } from 'next/router';
-
 
 
-import { ISelectableAll, ISelectableAndIndeterminatable } from '~/client/interfaces/selectable-all';
-import { IFormattedSearchResult } from '~/interfaces/search';
+import type { ISelectableAll, ISelectableAndIndeterminatable } from '~/client/interfaces/selectable-all';
+import { useKeywordManager } from '~/client/services/search-operation';
+import type { IFormattedSearchResult } from '~/interfaces/search';
 import { useIsSearchServiceReachable, useShowPageLimitationL } from '~/stores/context';
 import { useIsSearchServiceReachable, useShowPageLimitationL } from '~/stores/context';
-import { ISearchConditions, ISearchConfigurations, useSWRxSearch } from '~/stores/search';
+import { type ISearchConditions, type ISearchConfigurations, useSWRxSearch } from '~/stores/search';
 
 
 import { NotAvailableForGuest } from './NotAvailableForGuest';
 import { NotAvailableForGuest } from './NotAvailableForGuest';
 import { NotAvailableForReadOnlyUser } from './NotAvailableForReadOnlyUser';
 import { NotAvailableForReadOnlyUser } from './NotAvailableForReadOnlyUser';
 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 './SearchPage/SearchPageBase';
+import { type IReturnSelectedPageIds, SearchPageBase, usePageDeleteModalForBulkDeletion } from './SearchPage/SearchPageBase';
 
 
 
 
 // TODO: replace with "customize:showPageLimitationS"
 // TODO: replace with "customize:showPageLimitationS"
@@ -93,15 +91,8 @@ export const SearchPage = (): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const { data: showPageLimitationL } = useShowPageLimitationL();
   const { data: showPageLimitationL } = useShowPageLimitationL();
 
 
-  // routerRef solve the problem of infinite redrawing that occurs with routers
-  const router = useRouter();
-  const routerRef = useRef(router);
+  const { data: keyword, pushState } = useKeywordManager();
 
 
-  // parse URL Query
-  const queries = router.query.q;
-  const initQ = (Array.isArray(queries) ? queries.join(' ') : queries) ?? '';
-
-  const [keyword, setKeyword] = useState<string>(initQ);
   const [offset, setOffset] = useState<number>(0);
   const [offset, setOffset] = useState<number>(0);
   const [limit, setLimit] = useState<number>(showPageLimitationL ?? INITIAL_PAGIONG_SIZE);
   const [limit, setLimit] = useState<number>(showPageLimitationL ?? INITIAL_PAGIONG_SIZE);
   const [configurationsByControl, setConfigurationsByControl] = useState<Partial<ISearchConfigurations>>({});
   const [configurationsByControl, setConfigurationsByControl] = useState<Partial<ISearchConfigurations>>({});
@@ -110,17 +101,20 @@ export const SearchPage = (): JSX.Element => {
 
 
   const { data: isSearchServiceReachable } = useIsSearchServiceReachable();
   const { data: isSearchServiceReachable } = useIsSearchServiceReachable();
 
 
-  const { data, conditions, mutate } = useSWRxSearch(keyword, null, {
+  const { data, conditions, mutate } = useSWRxSearch(keyword ?? '', null, {
     ...configurationsByControl,
     ...configurationsByControl,
     offset,
     offset,
     limit,
     limit,
   });
   });
 
 
-  const searchInvokedHandler = useCallback((_keyword: string, newConfigurations: Partial<ISearchConfigurations>) => {
-    setKeyword(_keyword);
+  const searchInvokedHandler = useCallback((newKeyword: string, newConfigurations: Partial<ISearchConfigurations>) => {
     setOffset(0);
     setOffset(0);
     setConfigurationsByControl(newConfigurations);
     setConfigurationsByControl(newConfigurations);
-  }, []);
+
+    pushState(newKeyword);
+
+    mutate();
+  }, [keyword, mutate, pushState]);
 
 
   const selectAllCheckboxChangedHandler = useCallback((isChecked: boolean) => {
   const selectAllCheckboxChangedHandler = useCallback((isChecked: boolean) => {
     const instance = searchPageBaseRef.current;
     const instance = searchPageBaseRef.current;
@@ -176,25 +170,6 @@ export const SearchPage = (): JSX.Element => {
   // for bulk deletion
   // for bulk deletion
   const deleteAllButtonClickedHandler = usePageDeleteModalForBulkDeletion(data, searchPageBaseRef, () => mutate());
   const deleteAllButtonClickedHandler = usePageDeleteModalForBulkDeletion(data, searchPageBaseRef, () => mutate());
 
 
-  // push state
-  useEffect(() => {
-    const newUrl = new URL('/_search', 'http://example.com');
-    newUrl.searchParams.append('q', keyword);
-    routerRef.current.push(`${newUrl.pathname}${newUrl.search}`, '', { shallow: true });
-  }, [keyword, routerRef]);
-
-  // browser back and forward
-  useEffect(() => {
-    routerRef.current.beforePopState(({ url }) => {
-      const newUrl = new URL(url, 'https://exmple.com');
-      const newKeyword = newUrl.searchParams.get('q');
-      if (newKeyword != null) {
-        setKeyword(newKeyword);
-      }
-      return true;
-    });
-  }, [setKeyword, routerRef]);
-
   const hitsCount = data?.meta.hitsCount;
   const hitsCount = data?.meta.hitsCount;
 
 
   const allControl = useMemo(() => {
   const allControl = useMemo(() => {
@@ -246,7 +221,7 @@ export const SearchPage = (): JSX.Element => {
     return (
     return (
       <SearchResultListHead
       <SearchResultListHead
         searchResult={data}
         searchResult={data}
-        searchingKeyword={keyword}
+        searchingKeyword={keyword ?? ''}
         offset={offset}
         offset={offset}
         pagingSize={limit}
         pagingSize={limit}
         onPagingSizeChanged={pagingSizeChangedHandler}
         onPagingSizeChanged={pagingSizeChangedHandler}

+ 32 - 20
apps/app/src/components/SearchPage/SearchControl.tsx

@@ -18,7 +18,7 @@ type Props = {
   isEnableFilter: boolean,
   isEnableFilter: boolean,
   initialSearchConditions: Partial<ISearchConditions>,
   initialSearchConditions: Partial<ISearchConditions>,
 
 
-  onSearchInvoked: (keyword: string, configurations: Partial<ISearchConfigurations>) => void,
+  onSearchInvoked?: (keyword: string, configurations: Partial<ISearchConfigurations>) => void,
 
 
   allControl: React.ReactNode,
   allControl: React.ReactNode,
 }
 }
@@ -34,7 +34,9 @@ const SearchControl = React.memo((props: Props): JSX.Element => {
     allControl,
     allControl,
   } = props;
   } = props;
 
 
-  const [keyword, setKeyword] = useState(initialSearchConditions.keyword ?? '');
+  const keywordOnInit = initialSearchConditions.keyword ?? '';
+
+  const [keyword, setKeyword] = useState(keywordOnInit);
   const [sort, setSort] = useState<SORT_AXIS>(initialSearchConditions.sort ?? SORT_AXIS.RELATION_SCORE);
   const [sort, setSort] = useState<SORT_AXIS>(initialSearchConditions.sort ?? SORT_AXIS.RELATION_SCORE);
   const [order, setOrder] = useState<SORT_ORDER>(initialSearchConditions.order ?? SORT_ORDER.DESC);
   const [order, setOrder] = useState<SORT_ORDER>(initialSearchConditions.order ?? SORT_ORDER.DESC);
   const [includeUserPages, setIncludeUserPages] = useState(initialSearchConditions.includeUserPages ?? false);
   const [includeUserPages, setIncludeUserPages] = useState(initialSearchConditions.includeUserPages ?? false);
@@ -43,32 +45,42 @@ const SearchControl = React.memo((props: Props): JSX.Element => {
 
 
   const { t } = useTranslation('');
   const { t } = useTranslation('');
 
 
-  const invokeSearch = useCallback(() => {
-    if (onSearchInvoked == null) {
-      return;
-    }
+  const searchFormSubmittedHandler = useCallback((input: string) => {
+    setKeyword(input);
 
 
-    onSearchInvoked(keyword, {
+    onSearchInvoked?.(input, {
       sort, order, includeUserPages, includeTrashPages,
       sort, order, includeUserPages, includeTrashPages,
     });
     });
-  }, [keyword, sort, order, includeTrashPages, includeUserPages, onSearchInvoked]);
-
-  const searchFormSubmittedHandler = useCallback((input: string) => {
-    setKeyword(input);
-  }, []);
+  }, [includeTrashPages, includeUserPages, onSearchInvoked, order, sort]);
 
 
   const changeSortHandler = useCallback((nextSort: SORT_AXIS, nextOrder: SORT_ORDER) => {
   const changeSortHandler = useCallback((nextSort: SORT_AXIS, nextOrder: SORT_ORDER) => {
     setSort(nextSort);
     setSort(nextSort);
     setOrder(nextOrder);
     setOrder(nextOrder);
-  }, []);
 
 
-  useEffect(() => {
-    invokeSearch();
-  }, [invokeSearch]);
+    onSearchInvoked?.(keyword, {
+      sort: nextSort, order: nextOrder, includeUserPages, includeTrashPages,
+    });
+  }, [includeTrashPages, includeUserPages, keyword, onSearchInvoked]);
+
+  const changeIncludeUserPagesHandler = useCallback((include: boolean) => {
+    setIncludeUserPages(include);
+
+    onSearchInvoked?.(keyword, {
+      sort, order, includeUserPages: include, includeTrashPages,
+    });
+  }, [includeTrashPages, keyword, onSearchInvoked, order, sort]);
+
+  const changeIncludeTrashPagesHandler = useCallback((include: boolean) => {
+    setIncludeTrashPages(include);
+
+    onSearchInvoked?.(keyword, {
+      sort, order, includeUserPages, includeTrashPages: include,
+    });
+  }, [includeUserPages, keyword, onSearchInvoked, order, sort]);
 
 
   useEffect(() => {
   useEffect(() => {
-    setKeyword(initialSearchConditions.keyword ?? '');
-  }, [initialSearchConditions.keyword]);
+    setKeyword(keywordOnInit);
+  }, [keywordOnInit]);
 
 
   return (
   return (
     <div className="position-sticky sticky-top shadow-sm">
     <div className="position-sticky sticky-top shadow-sm">
@@ -128,7 +140,7 @@ const SearchControl = React.memo((props: Props): JSX.Element => {
                     type="checkbox"
                     type="checkbox"
                     id="flexCheckDefault"
                     id="flexCheckDefault"
                     defaultChecked={includeUserPages}
                     defaultChecked={includeUserPages}
-                    onChange={e => setIncludeUserPages(e.target.checked)}
+                    onChange={e => changeIncludeUserPagesHandler(e.target.checked)}
                   />
                   />
                   <label className="custom-control-label mb-0 d-flex align-items-center text-secondary with-no-font-weight" htmlFor="flexCheckDefault">
                   <label className="custom-control-label mb-0 d-flex align-items-center text-secondary with-no-font-weight" htmlFor="flexCheckDefault">
                     {t('Include Subordinated Target Page', { target: '/user' })}
                     {t('Include Subordinated Target Page', { target: '/user' })}
@@ -142,7 +154,7 @@ const SearchControl = React.memo((props: Props): JSX.Element => {
                     type="checkbox"
                     type="checkbox"
                     id="flexCheckChecked"
                     id="flexCheckChecked"
                     checked={includeTrashPages}
                     checked={includeTrashPages}
-                    onChange={e => setIncludeTrashPages(e.target.checked)}
+                    onChange={e => changeIncludeTrashPagesHandler(e.target.checked)}
                   />
                   />
                   <label
                   <label
                     className="custom-control-label
                     className="custom-control-label

+ 4 - 1
apps/app/src/components/Sidebar/CustomSidebar.tsx

@@ -20,10 +20,13 @@ const logger = loggerFactory('growi:cli:CustomSidebar');
 
 
 
 
 const SidebarNotFound = () => {
 const SidebarNotFound = () => {
+  const { t } = useTranslation();
+
   return (
   return (
     <div className="grw-sidebar-content-header h5 text-center py-3">
     <div className="grw-sidebar-content-header h5 text-center py-3">
       <Link href="/Sidebar#edit">
       <Link href="/Sidebar#edit">
-        <i className="icon-magic-wand"></i>Create<strong>/Sidebar</strong>page
+        <i className="icon-fw icon-magic-wand"></i>
+        <span dangerouslySetInnerHTML={{ __html: t('Create Sidebar Page') }}></span>
       </Link>
       </Link>
     </div>
     </div>
   );
   );

+ 10 - 7
apps/app/src/components/Sidebar/RecentChanges.tsx

@@ -6,8 +6,8 @@ import { isPopulated, type IPageHasId } from '@growi/core';
 import { DevidedPagePath } from '@growi/core/dist/models';
 import { DevidedPagePath } from '@growi/core/dist/models';
 import { UserPicture, FootstampIcon } from '@growi/ui/dist/components';
 import { UserPicture, FootstampIcon } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
-import Link from 'next/link';
 
 
+import { useKeywordManager } from '~/client/services/search-operation';
 import PagePathHierarchicalLink from '~/components/PagePathHierarchicalLink';
 import PagePathHierarchicalLink from '~/components/PagePathHierarchicalLink';
 import LinkedPagePath from '~/models/linked-page-path';
 import LinkedPagePath from '~/models/linked-page-path';
 import { useSWRINFxRecentlyUpdated } from '~/stores/page-listing';
 import { useSWRINFxRecentlyUpdated } from '~/stores/page-listing';
@@ -30,6 +30,7 @@ type PageItemLowerProps = {
 
 
 type PageItemProps = PageItemLowerProps & {
 type PageItemProps = PageItemLowerProps & {
   isSmall: boolean
   isSmall: boolean
+  onClickTag?: (tagName: string) => void,
 }
 }
 
 
 const PageItemLower = memo(({ page }: PageItemLowerProps): JSX.Element => {
 const PageItemLower = memo(({ page }: PageItemLowerProps): JSX.Element => {
@@ -49,7 +50,7 @@ const PageItemLower = memo(({ page }: PageItemLowerProps): JSX.Element => {
 });
 });
 PageItemLower.displayName = 'PageItemLower';
 PageItemLower.displayName = 'PageItemLower';
 
 
-const PageItem = memo(({ page, isSmall }: PageItemProps): JSX.Element => {
+const PageItem = memo(({ page, isSmall, onClickTag }: PageItemProps): JSX.Element => {
   const dPagePath = new DevidedPagePath(page.path, false, true);
   const dPagePath = new DevidedPagePath(page.path, false, true);
   const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
   const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
   const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
   const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
@@ -70,14 +71,14 @@ const PageItem = memo(({ page, isSmall }: PageItemProps): JSX.Element => {
       return <></>;
       return <></>;
     }
     }
     return (
     return (
-      <Link
+      <a
         key={tag.name}
         key={tag.name}
-        href={`/_search?q=tag:${tag.name}`}
+        type="button"
         className="grw-tag-label badge badge-secondary mr-2 small"
         className="grw-tag-label badge badge-secondary mr-2 small"
-        prefetch={false}
+        onClick={() => onClickTag?.(tag.name)}
       >
       >
         {tag.name}
         {tag.name}
-      </Link>
+      </a>
     );
     );
   });
   });
 
 
@@ -113,6 +114,8 @@ const RecentChanges = (): JSX.Element => {
     data, mutate, isLoading,
     data, mutate, isLoading,
   } = swrInifinitexRecentlyUpdated;
   } = swrInifinitexRecentlyUpdated;
 
 
+  const { pushState } = useKeywordManager();
+
   const [isRecentChangesSidebarSmall, setIsRecentChangesSidebarSmall] = useState(false);
   const [isRecentChangesSidebarSmall, setIsRecentChangesSidebarSmall] = useState(false);
   const isEmpty = data?.[0]?.pages.length === 0;
   const isEmpty = data?.[0]?.pages.length === 0;
   const isReachingEnd = isEmpty || (data != null && data[data.length - 1]?.pages.length < PER_PAGE);
   const isReachingEnd = isEmpty || (data != null && data[data.length - 1]?.pages.length < PER_PAGE);
@@ -162,7 +165,7 @@ const RecentChanges = (): JSX.Element => {
               >
               >
                 { data != null && data.map(apiResult => apiResult.pages).flat()
                 { data != null && data.map(apiResult => apiResult.pages).flat()
                   .map(page => (
                   .map(page => (
-                    <PageItem key={page._id} page={page} isSmall={isRecentChangesSidebarSmall} />
+                    <PageItem key={page._id} page={page} isSmall={isRecentChangesSidebarSmall} onClickTag={tagName => pushState(`tag:${tagName}`)} />
                   ))
                   ))
                 }
                 }
               </InfiniteScroll>
               </InfiniteScroll>

+ 7 - 9
apps/app/src/components/TagCloudBox.tsx

@@ -1,7 +1,6 @@
 import React, { FC, memo } from 'react';
 import React, { FC, memo } from 'react';
 
 
-import Link from 'next/link';
-
+import { useKeywordManager } from '~/client/services/search-operation';
 import { IDataTagCount } from '~/interfaces/tag';
 import { IDataTagCount } from '~/interfaces/tag';
 
 
 
 
@@ -23,21 +22,20 @@ const TagCloudBox: FC<Props> = memo((props:(Props & typeof defaultProps)) => {
   const { tags } = props;
   const { tags } = props;
   const maxTagTextLength: number = props.maxTagTextLength ?? MAX_TAG_TEXT_LENGTH;
   const maxTagTextLength: number = props.maxTagTextLength ?? MAX_TAG_TEXT_LENGTH;
 
 
+  const { pushState } = useKeywordManager();
+
   const tagElements = tags.map((tag:IDataTagCount) => {
   const tagElements = tags.map((tag:IDataTagCount) => {
     const tagNameFormat = (tag.name).length > maxTagTextLength ? `${(tag.name).slice(0, maxTagTextLength)}...` : tag.name;
     const tagNameFormat = (tag.name).length > maxTagTextLength ? `${(tag.name).slice(0, maxTagTextLength)}...` : tag.name;
 
 
-    const url = new URL('/_search', 'https://example.com');
-    url.searchParams.append('q', `tag:${tag.name}`);
-
     return (
     return (
-      <Link
+      <a
         key={tag.name}
         key={tag.name}
-        href={`${url.pathname}${url.search}`}
+        type="button"
         className="grw-tag-label badge badge-secondary mr-2"
         className="grw-tag-label badge badge-secondary mr-2"
-        prefetch={false}
+        onClick={() => pushState(`tag:${tag.name}`)}
       >
       >
         {tagNameFormat}
         {tagNameFormat}
-      </Link>
+      </a>
     );
     );
   });
   });
 
 

+ 12 - 15
apps/app/src/components/TagList.tsx

@@ -3,8 +3,8 @@ import React, {
 } from 'react';
 } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
-import Link from 'next/link';
 
 
+import { useKeywordManager } from '~/client/services/search-operation';
 import { IDataTagCount } from '~/interfaces/tag';
 import { IDataTagCount } from '~/interfaces/tag';
 
 
 import PaginationWrapper from './PaginationWrapper';
 import PaginationWrapper from './PaginationWrapper';
@@ -29,26 +29,23 @@ const TagList: FC<TagListProps> = (props:(TagListProps & typeof defaultProps)) =
   const isTagExist: boolean = tagData.length > 0;
   const isTagExist: boolean = tagData.length > 0;
   const { t } = useTranslation('');
   const { t } = useTranslation('');
 
 
-  const generateTagList = useCallback((tagData) => {
-    return tagData.map((tag:IDataTagCount, index:number) => {
-      const tagListClasses: string = index === 0 ? 'list-group-item d-flex' : 'list-group-item d-flex border-top-0';
-
-      const url = new URL('/_search', 'https://example.com');
-      url.searchParams.append('q', `tag:${tag.name}`);
+  const { pushState } = useKeywordManager();
 
 
+  const generateTagList = useCallback((tagData) => {
+    return tagData.map((tag:IDataTagCount) => {
       return (
       return (
-        <Link
+        <button
           key={tag._id}
           key={tag._id}
-          href={`${url.pathname}${url.search}`}
-          className={tagListClasses}
-          prefetch={false}
+          type="button"
+          className="list-group-item list-group-item-action d-flex"
+          onClick={() => pushState(`tag:${tag.name}`)}
         >
         >
           <div className="text-truncate list-tag-name">{tag.name}</div>
           <div className="text-truncate list-tag-name">{tag.name}</div>
           <div className="ml-4 my-auto py-1 px-2 list-tag-count badge badge-secondary text-white">{tag.count}</div>
           <div className="ml-4 my-auto py-1 px-2 list-tag-count badge badge-secondary text-white">{tag.count}</div>
-        </Link>
+        </button>
       );
       );
     });
     });
-  }, []);
+  }, [pushState]);
 
 
   if (!isTagExist) {
   if (!isTagExist) {
     return <h3>{ t('You have no tag, You can set tags on pages') }</h3>;
     return <h3>{ t('You have no tag, You can set tags on pages') }</h3>;
@@ -56,9 +53,9 @@ const TagList: FC<TagListProps> = (props:(TagListProps & typeof defaultProps)) =
 
 
   return (
   return (
     <>
     <>
-      <ul className="list-group text-left mb-5">
+      <div className="list-group text-left mb-5">
         {generateTagList(tagData)}
         {generateTagList(tagData)}
-      </ul>
+      </div>
       {isPaginationShown
       {isPaginationShown
       && (
       && (
         <PaginationWrapper
         <PaginationWrapper

+ 24 - 4
apps/app/src/server/routes/apiv3/customize-setting.js

@@ -61,8 +61,6 @@ const router = express.Router();
  *            type: boolean
  *            type: boolean
  *          isSearchScopeChildrenAsDefault:
  *          isSearchScopeChildrenAsDefault:
  *            type: boolean
  *            type: boolean
- *          isEnabledMarp:
- *            type: boolean
  *      CustomizeHighlight:
  *      CustomizeHighlight:
  *        description: CustomizeHighlight
  *        description: CustomizeHighlight
  *        type: object
  *        type: object
@@ -127,6 +125,8 @@ module.exports = (crowi) => {
       body('isEnabledStaleNotification').isBoolean(),
       body('isEnabledStaleNotification').isBoolean(),
       body('isAllReplyShown').isBoolean(),
       body('isAllReplyShown').isBoolean(),
       body('isSearchScopeChildrenAsDefault').isBoolean(),
       body('isSearchScopeChildrenAsDefault').isBoolean(),
+    ],
+    CustomizePresentation: [
       body('isEnabledMarp').isBoolean(),
       body('isEnabledMarp').isBoolean(),
     ],
     ],
     customizeTitle: [
     customizeTitle: [
@@ -411,7 +411,6 @@ module.exports = (crowi) => {
       'customize:isEnabledStaleNotification': req.body.isEnabledStaleNotification,
       'customize:isEnabledStaleNotification': req.body.isEnabledStaleNotification,
       'customize:isAllReplyShown': req.body.isAllReplyShown,
       'customize:isAllReplyShown': req.body.isAllReplyShown,
       'customize:isSearchScopeChildrenAsDefault': req.body.isSearchScopeChildrenAsDefault,
       'customize:isSearchScopeChildrenAsDefault': req.body.isSearchScopeChildrenAsDefault,
-      'customize:isEnabledMarp': req.body.isEnabledMarp,
     };
     };
 
 
     try {
     try {
@@ -426,7 +425,6 @@ module.exports = (crowi) => {
         isEnabledStaleNotification: await crowi.configManager.getConfig('crowi', 'customize:isEnabledStaleNotification'),
         isEnabledStaleNotification: await crowi.configManager.getConfig('crowi', 'customize:isEnabledStaleNotification'),
         isAllReplyShown: await crowi.configManager.getConfig('crowi', 'customize:isAllReplyShown'),
         isAllReplyShown: await crowi.configManager.getConfig('crowi', 'customize:isAllReplyShown'),
         isSearchScopeChildrenAsDefault: await crowi.configManager.getConfig('crowi', 'customize:isSearchScopeChildrenAsDefault'),
         isSearchScopeChildrenAsDefault: await crowi.configManager.getConfig('crowi', 'customize:isSearchScopeChildrenAsDefault'),
-        isEnabledMarp: await crowi.configManager.getConfig('crowi', 'customize:isEnabledMarp'),
       };
       };
       const parameters = { action: SupportedAction.ACTION_ADMIN_FUNCTION_UPDATE };
       const parameters = { action: SupportedAction.ACTION_ADMIN_FUNCTION_UPDATE };
       activityEvent.emit('update', res.locals.activity._id, parameters);
       activityEvent.emit('update', res.locals.activity._id, parameters);
@@ -439,6 +437,28 @@ module.exports = (crowi) => {
     }
     }
   });
   });
 
 
+
+  router.put('/presentation', loginRequiredStrictly, adminRequired, addActivity, validator.CustomizePresentation, apiV3FormValidator, async(req, res) => {
+    const requestParams = {
+      'customize:isEnabledMarp': req.body.isEnabledMarp,
+    };
+
+    try {
+      await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
+      const customizedParams = {
+        isEnabledMarp: await crowi.configManager.getConfig('crowi', 'customize:isEnabledMarp'),
+      };
+      const parameters = { action: SupportedAction.ACTION_ADMIN_FUNCTION_UPDATE };
+      activityEvent.emit('update', res.locals.activity._id, parameters);
+      return res.apiv3({ customizedParams });
+    }
+    catch (err) {
+      const msg = 'Error occurred in updating presentaion';
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(msg, 'update-presentation-failed'));
+    }
+  });
+
   /**
   /**
    * @swagger
    * @swagger
    *
    *

+ 2 - 1
apps/app/src/server/routes/apiv3/users.js

@@ -188,11 +188,12 @@ module.exports = (crowi) => {
   const sendEmailByUser = async(user) => {
   const sendEmailByUser = async(user) => {
     const { appService, mailService } = crowi;
     const { appService, mailService } = crowi;
     const appTitle = appService.getAppTitle();
     const appTitle = appService.getAppTitle();
+    const locale = configManager.getConfig('crowi', 'app:globalLang');
 
 
     await mailService.send({
     await mailService.send({
       to: user.email,
       to: user.email,
       subject: `New password for ${appTitle}`,
       subject: `New password for ${appTitle}`,
-      template: path.join(crowi.localeDir, 'en_US/admin/userResetPassword.ejs'),
+      template: path.join(crowi.localeDir, `${locale}/admin/userResetPassword.ejs`),
       vars: {
       vars: {
         email: user.email,
         email: user.email,
         password: user.password,
         password: user.password,

+ 3 - 1
apps/app/src/server/routes/attachment.js

@@ -264,7 +264,9 @@ module.exports = function(crowi, app) {
     else {
     else {
       res.set({
       res.set({
         'Content-Type': attachment.fileFormat,
         'Content-Type': attachment.fileFormat,
-        'Content-Security-Policy': "script-src 'unsafe-hashes'; object-src 'none'; require-trusted-types-for 'script'; media-src 'self'; default-src 'none';",
+        // eslint-disable-next-line max-len
+        'Content-Security-Policy': "script-src 'unsafe-hashes'; style-src 'self' 'unsafe-inline'; object-src 'none'; require-trusted-types-for 'script'; media-src 'self'; default-src 'none';",
+        'Content-Disposition': `inline;filename*=UTF-8''${encodeURIComponent(attachment.originalName)}`,
       });
       });
     }
     }
   }
   }

+ 13 - 4
apps/app/src/stores/tag.tsx

@@ -1,19 +1,28 @@
-import { SWRResponse } from 'swr';
-import useSWRImmutable from 'swr/immutable';
+import useSWR, { SWRResponse } from 'swr';
 
 
 import { apiGet } from '~/client/util/apiv1-client';
 import { apiGet } from '~/client/util/apiv1-client';
 import { IResTagsListApiv1, IResTagsSearchApiv1 } from '~/interfaces/tag';
 import { IResTagsListApiv1, IResTagsSearchApiv1 } from '~/interfaces/tag';
 
 
 export const useSWRxTagsList = (limit?: number, offset?: number): SWRResponse<IResTagsListApiv1, Error> => {
 export const useSWRxTagsList = (limit?: number, offset?: number): SWRResponse<IResTagsListApiv1, Error> => {
-  return useSWRImmutable(
+  return useSWR(
     ['/tags.list', limit, offset],
     ['/tags.list', limit, offset],
     ([endpoint, limit, offset]) => apiGet(endpoint, { limit, offset }).then((result: IResTagsListApiv1) => result),
     ([endpoint, limit, offset]) => apiGet(endpoint, { limit, offset }).then((result: IResTagsListApiv1) => result),
+    {
+      keepPreviousData: true,
+      revalidateOnFocus: false,
+      revalidateOnReconnect: false,
+    },
   );
   );
 };
 };
 
 
 export const useSWRxTagsSearch = (query: string): SWRResponse<IResTagsSearchApiv1, Error> => {
 export const useSWRxTagsSearch = (query: string): SWRResponse<IResTagsSearchApiv1, Error> => {
-  return useSWRImmutable(
+  return useSWR(
     ['/tags.search', query],
     ['/tags.search', query],
     ([endpoint, query]) => apiGet(endpoint, { q: query }).then((result: IResTagsSearchApiv1) => result),
     ([endpoint, query]) => apiGet(endpoint, { q: query }).then((result: IResTagsSearchApiv1) => result),
+    {
+      keepPreviousData: true,
+      revalidateOnFocus: false,
+      revalidateOnReconnect: false,
+    },
   );
   );
 };
 };

+ 2 - 1
apps/app/test/cypress/e2e/20-basic-features/20-basic-features--sticky-features.cy.ts

@@ -111,7 +111,8 @@ context('Access to any page', () => {
     });
     });
     cy.waitUntil(() => {
     cy.waitUntil(() => {
       cy.getByTestid('grw-subnav-switcher').within(() => {
       cy.getByTestid('grw-subnav-switcher').within(() => {
-        cy.getByTestid('editor-button').should('be.visible').click();
+        cy.getByTestid('editor-button').as('editorButton').should('be.visible');
+        cy.get('@editorButton').click();
       });
       });
       return cy.get('.layout-root').then($elem => $elem.hasClass('editing'));
       return cy.get('.layout-root').then($elem => $elem.hasClass('editing'));
     });
     });

+ 1 - 1
apps/slackbot-proxy/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/slackbot-proxy",
   "name": "@growi/slackbot-proxy",
-  "version": "6.2.0-slackbot-proxy.0",
+  "version": "6.2.1-slackbot-proxy.0",
   "license": "MIT",
   "license": "MIT",
   "scripts": {
   "scripts": {
     "build": "yarn tsc && tsc-alias -p tsconfig.build.json",
     "build": "yarn tsc && tsc-alias -p tsconfig.build.json",

+ 1 - 1
apps/slackbot-proxy/src/views/top.ejs

@@ -38,7 +38,7 @@
           </ul>
           </ul>
         <div class="mt-3">
         <div class="mt-3">
           GROWI is open-source software developed by WESEEK, Inc and we are looking for contributors who can work with us.<br>
           GROWI is open-source software developed by WESEEK, Inc and we are looking for contributors who can work with us.<br>
-          Please <a href="https://growi-slackin.weseek.co.jp/">join</a> Slack and feel free to talk to WESEEK members!
+          Please <a href="https://communityinviter.com/apps/wsgrowi/invite/">join</a> Slack and feel free to talk to WESEEK members!
         </div>
         </div>
       </div>
       </div>
     </div>
     </div>

+ 1 - 1
package.json

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

+ 1 - 1
packages/core/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/core",
   "name": "@growi/core",
-  "version": "6.2.0-RC.0",
+  "version": "6.2.1-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.2.0-RC.0",
+  "version": "6.2.1-RC.0",
   "description": "GROWI js and css files to use hackmd",
   "description": "GROWI js and css files to use hackmd",
   "license": "MIT",
   "license": "MIT",
   "type": "module",
   "type": "module",

+ 8 - 6
packages/presentation/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/presentation",
   "name": "@growi/presentation",
-  "version": "6.2.0-RC.0",
+  "version": "6.2.1-RC.0",
   "description": "GROWI plugin for presentation",
   "description": "GROWI plugin for presentation",
   "license": "MIT",
   "license": "MIT",
   "keywords": [
   "keywords": [
@@ -31,21 +31,23 @@
     "@marp-team/marp-core": "^3.6.0",
     "@marp-team/marp-core": "^3.6.0",
     "@types/reveal.js": "^4.4.1",
     "@types/reveal.js": "^4.4.1",
     "eslint-plugin-regex": "^1.8.0",
     "eslint-plugin-regex": "^1.8.0",
-    "reveal.js": "^4.4.0",
+    "hast-util-sanitize": "^4.1.0",
+    "hast-util-select": "^5.0.5",
     "mdast-util-frontmatter": "^1.0.0",
     "mdast-util-frontmatter": "^1.0.0",
     "mdast-util-gfm": "^2.0.1",
     "mdast-util-gfm": "^2.0.1",
     "mdast-util-to-markdown": "^1.3.0",
     "mdast-util-to-markdown": "^1.3.0",
-    "hast-util-sanitize": "^4.1.0",
-    "hast-util-select": "^5.0.5",
     "react-markdown": "^8.0.7",
     "react-markdown": "^8.0.7",
+    "remark-frontmatter": "^4.0.1",
+    "remark-stringify": "^10.0.0",
+    "reveal.js": "^4.4.0",
     "unified": "^10.1.2",
     "unified": "^10.1.2",
     "unist-util-find-after": "^4.0.0",
     "unist-util-find-after": "^4.0.0",
     "unist-util-visit": "^4.0.0"
     "unist-util-visit": "^4.0.0"
   },
   },
   "peerDependencies": {
   "peerDependencies": {
+    "@marp-team/marpit": "*",
     "next": "^13",
     "next": "^13",
     "react": "^18.2.0",
     "react": "^18.2.0",
-    "react-dom": "^18.2.0",
-    "@marp-team/marpit": "*"
+    "react-dom": "^18.2.0"
   }
   }
 }
 }

+ 0 - 2
packages/presentation/src/components/GrowiSlides.tsx

@@ -5,8 +5,6 @@ import type { PresentationOptions } from '../consts';
 import { MARP_CONTAINER_CLASS_NAME, presentationMarpit, slideMarpit } from '../services/growi-marpit';
 import { MARP_CONTAINER_CLASS_NAME, presentationMarpit, slideMarpit } from '../services/growi-marpit';
 import * as extractSections from '../services/renderer/extract-sections';
 import * as extractSections from '../services/renderer/extract-sections';
 
 
-
-import './Slides.global.scss';
 import { PresentationRichSlideSection, RichSlideSection } from './RichSlideSection';
 import { PresentationRichSlideSection, RichSlideSection } from './RichSlideSection';
 
 
 
 

+ 0 - 1
packages/presentation/src/components/MarpSlides.tsx

@@ -1,6 +1,5 @@
 import Head from 'next/head';
 import Head from 'next/head';
 
 
-import './Slides.global.scss';
 import { presentationMarpit, slideMarpit } from '../services/growi-marpit';
 import { presentationMarpit, slideMarpit } from '../services/growi-marpit';
 
 
 type Props = {
 type Props = {

+ 0 - 4
packages/presentation/src/components/Slides.global.scss

@@ -1,4 +0,0 @@
-div.slides.marpit > section :is(pre, marp-pre) {
-  padding: 0;
-  border: none;
-}

+ 35 - 0
packages/presentation/src/components/Slides.module.scss

@@ -0,0 +1,35 @@
+.slides-styles :global {
+  // Reset _wiki.scss, vendor.scss and other css in <div class=slides marpit>.
+  // This ensures that Marp is rendered currently.
+  .slides {
+    *, *:before, *::after {
+      box-sizing: initial;
+    }
+
+    *::before, *::after {
+      all: initial;
+    }
+
+    h1,
+    h2,
+    h3,
+    h4,
+    h5,
+    h6,
+    p {
+      font-weight: initial;
+      line-height: initial;
+      border: initial;
+    }
+
+    code {
+      color: unset;
+    }
+
+  }
+
+  section {
+    padding: 0;
+    border: none;
+  }
+}

+ 10 - 6
packages/presentation/src/components/Slides.tsx

@@ -4,23 +4,27 @@ import type { PresentationOptions } from '../consts';
 import { GrowiSlides } from './GrowiSlides';
 import { GrowiSlides } from './GrowiSlides';
 import { MarpSlides } from './MarpSlides';
 import { MarpSlides } from './MarpSlides';
 
 
-import './Slides.global.scss';
+import styles from './Slides.module.scss';
 
 
-type Props = {
+export type SlidesProps = {
   options: PresentationOptions,
   options: PresentationOptions,
   children?: string,
   children?: string,
   hasMarpFlag?: boolean,
   hasMarpFlag?: boolean,
   presentation?: boolean,
   presentation?: boolean,
 }
 }
 
 
-export const Slides = (props: Props): JSX.Element => {
+export const Slides = (props: SlidesProps): JSX.Element => {
   const {
   const {
     options, children, hasMarpFlag, presentation,
     options, children, hasMarpFlag, presentation,
   } = props;
   } = props;
 
 
   return (
   return (
-    hasMarpFlag
-      ? <MarpSlides presentation={presentation}>{children}</MarpSlides>
-      : <GrowiSlides options={options} presentation={presentation}>{children}</GrowiSlides>
+    <div className={`${styles['slides-styles']}`}>
+      {
+        hasMarpFlag
+          ? <MarpSlides presentation={presentation}>{children}</MarpSlides>
+          : <GrowiSlides options={options} presentation={presentation}>{children}</GrowiSlides>
+      }
+    </div>
   );
   );
 };
 };

+ 16 - 18
packages/presentation/src/services/growi-marpit.ts

@@ -1,4 +1,4 @@
-import { Marp } from '@marp-team/marp-core';
+import { Marp, MarpOptions } from '@marp-team/marp-core';
 import { Element } from '@marp-team/marpit';
 import { Element } from '@marp-team/marpit';
 
 
 export const MARP_CONTAINER_CLASS_NAME = 'marpit';
 export const MARP_CONTAINER_CLASS_NAME = 'marpit';
@@ -36,28 +36,26 @@ const lineNumber = (md) => {
   });
   });
 };
 };
 
 
-export const slideMarpit = new Marp({
+const marpitOption: MarpOptions = {
   container: [
   container: [
     new Element('div', { class: `slides ${MARP_CONTAINER_CLASS_NAME}` }),
     new Element('div', { class: `slides ${MARP_CONTAINER_CLASS_NAME}` }),
   ],
   ],
-  slideContainer: [
-    new Element('section', { class: 'shadow rounded m-2' }),
-  ],
   inlineSVG: true,
   inlineSVG: true,
   emoji: undefined,
   emoji: undefined,
   html: false,
   html: false,
   math: false,
   math: false,
-}).use(lineNumber);
+};
 
 
-export const presentationMarpit = new Marp({
-  container: [
-    new Element('div', { class: `slides ${MARP_CONTAINER_CLASS_NAME}` }),
-  ],
-  slideContainer: [
-    new Element('section', { class: 'm-2' }),
-  ],
-  inlineSVG: true,
-  emoji: undefined,
-  html: false,
-  math: false,
-});
+const slideMarpitOption = marpitOption;
+slideMarpitOption.slideContainer = (
+  [new Element('section', { class: 'shadow rounded m-2' })]
+);
+
+export const slideMarpit = new Marp(slideMarpitOption).use(lineNumber);
+
+const presentationMarpitOption = marpitOption;
+presentationMarpitOption.slideContainer = (
+  [new Element('section', { class: 'm-2' })]
+);
+
+export const presentationMarpit = new Marp(presentationMarpitOption);

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

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/preset-templates",
   "name": "@growi/preset-templates",
-  "version": "6.2.0-RC.0",
+  "version": "6.2.1-RC.0",
   "scripts": {
   "scripts": {
     "test": "vitest run",
     "test": "vitest run",
     "version": "yarn version --no-git-tag-version --preid=RC"
     "version": "yarn version --no-git-tag-version --preid=RC"

+ 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.2.0-RC.0",
+  "version": "6.2.1-RC.0",
   "license": "MIT",
   "license": "MIT",
   "main": "dist/libs/preset-themes.umd.js",
   "main": "dist/libs/preset-themes.umd.js",
   "module": "dist/libs/preset-themes.mjs",
   "module": "dist/libs/preset-themes.mjs",

+ 3 - 3
packages/remark-attachment-refs/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/remark-attachment-refs",
   "name": "@growi/remark-attachment-refs",
-  "version": "6.2.0-RC.0",
+  "version": "6.2.1-RC.0",
   "description": "GROWI Plugin to add ref/refimg/refs/refsimg tags",
   "description": "GROWI Plugin to add ref/refimg/refs/refsimg tags",
   "license": "MIT",
   "license": "MIT",
   "keywords": [
   "keywords": [
@@ -48,8 +48,8 @@
     "axios": "^0.24.0",
     "axios": "^0.24.0",
     "bunyan": "^1.8.15",
     "bunyan": "^1.8.15",
     "hast-util-select": "^5.0.5",
     "hast-util-select": "^5.0.5",
-    "universal-bunyan": "^0.9.2",
-    "swr": "^2.0.3"
+    "swr": "^2.0.3",
+    "universal-bunyan": "^0.9.2"
   },
   },
   "devDependencies": {
   "devDependencies": {
     "csstype": "^3.1.2",
     "csstype": "^3.1.2",

+ 2 - 3
packages/remark-drawio/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/remark-drawio",
   "name": "@growi/remark-drawio",
-  "version": "6.2.0-RC.0",
+  "version": "6.2.1-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": [
@@ -30,8 +30,7 @@
     "lint": "run-p lint:*",
     "lint": "run-p lint:*",
     "version": "yarn version --no-git-tag-version --preid=RC"
     "version": "yarn version --no-git-tag-version --preid=RC"
   },
   },
-  "dependencies": {
-  },
+  "dependencies": {},
   "devDependencies": {
   "devDependencies": {
     "eslint-plugin-regex": "^1.8.0",
     "eslint-plugin-regex": "^1.8.0",
     "hast-util-sanitize": "^4.1.0",
     "hast-util-sanitize": "^4.1.0",

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

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/remark-growi-directive",
   "name": "@growi/remark-growi-directive",
-  "version": "6.2.0-RC.0",
+  "version": "6.2.1-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": [

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

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/remark-lsx",
   "name": "@growi/remark-lsx",
-  "version": "6.2.0-RC.0",
+  "version": "6.2.1-RC.0",
   "description": "GROWI plugin to list pages",
   "description": "GROWI plugin to list pages",
   "license": "MIT",
   "license": "MIT",
   "keywords": [
   "keywords": [

+ 1 - 1
packages/slack/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/slack",
   "name": "@growi/slack",
-  "version": "6.2.0-RC.0",
+  "version": "6.2.1-RC.0",
   "license": "MIT",
   "license": "MIT",
   "type": "module",
   "type": "module",
   "main": "dist/index.cjs",
   "main": "dist/index.cjs",

+ 1 - 1
packages/ui/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/ui",
   "name": "@growi/ui",
-  "version": "6.2.0-RC.0",
+  "version": "6.2.1-RC.0",
   "description": "GROWI UI Libraries",
   "description": "GROWI UI Libraries",
   "license": "MIT",
   "license": "MIT",
   "keywords": [
   "keywords": [

+ 11 - 11
yarn.lock

@@ -2500,13 +2500,13 @@
     xdg-basedir "^4.0.0"
     xdg-basedir "^4.0.0"
 
 
 "@growi/core@link:packages/core":
 "@growi/core@link:packages/core":
-  version "6.2.0-RC.0"
+  version "6.2.1-RC.0"
   dependencies:
   dependencies:
     bson-objectid "^2.0.4"
     bson-objectid "^2.0.4"
     escape-string-regexp "^4.0.0"
     escape-string-regexp "^4.0.0"
 
 
 "@growi/hackmd@link:packages/hackmd":
 "@growi/hackmd@link:packages/hackmd":
-  version "6.2.0-RC.0"
+  version "6.2.1-RC.0"
 
 
 "@growi/pluginkit@link:packages/pluginkit":
 "@growi/pluginkit@link:packages/pluginkit":
   version "0.1.0"
   version "0.1.0"
@@ -2515,18 +2515,18 @@
     extensible-custom-error "^0.0.7"
     extensible-custom-error "^0.0.7"
 
 
 "@growi/presentation@link:packages/presentation":
 "@growi/presentation@link:packages/presentation":
-  version "6.2.0-RC.0"
+  version "6.2.1-RC.0"
   dependencies:
   dependencies:
     "@growi/core" "link:packages/core"
     "@growi/core" "link:packages/core"
 
 
 "@growi/preset-templates@link:packages/preset-templates":
 "@growi/preset-templates@link:packages/preset-templates":
-  version "6.2.0-RC.0"
+  version "6.2.1-RC.0"
 
 
 "@growi/preset-themes@link:packages/preset-themes":
 "@growi/preset-themes@link:packages/preset-themes":
-  version "6.2.0-RC.0"
+  version "6.2.1-RC.0"
 
 
 "@growi/remark-attachment-refs@link:packages/remark-attachment-refs":
 "@growi/remark-attachment-refs@link:packages/remark-attachment-refs":
-  version "6.2.0-RC.0"
+  version "6.2.1-RC.0"
   dependencies:
   dependencies:
     "@growi/core" "link:packages/core"
     "@growi/core" "link:packages/core"
     "@growi/remark-growi-directive" "link:packages/remark-growi-directive"
     "@growi/remark-growi-directive" "link:packages/remark-growi-directive"
@@ -2538,10 +2538,10 @@
     universal-bunyan "^0.9.2"
     universal-bunyan "^0.9.2"
 
 
 "@growi/remark-drawio@link:packages/remark-drawio":
 "@growi/remark-drawio@link:packages/remark-drawio":
-  version "6.2.0-RC.0"
+  version "6.2.1-RC.0"
 
 
 "@growi/remark-growi-directive@link:packages/remark-growi-directive":
 "@growi/remark-growi-directive@link:packages/remark-growi-directive":
-  version "6.2.0-RC.0"
+  version "6.2.1-RC.0"
   dependencies:
   dependencies:
     "@types/mdast" "^3.0.0"
     "@types/mdast" "^3.0.0"
     "@types/unist" "^2.0.0"
     "@types/unist" "^2.0.0"
@@ -2558,7 +2558,7 @@
     uvu "^0.5.0"
     uvu "^0.5.0"
 
 
 "@growi/remark-lsx@link:packages/remark-lsx":
 "@growi/remark-lsx@link:packages/remark-lsx":
-  version "6.2.0-RC.0"
+  version "6.2.1-RC.0"
   dependencies:
   dependencies:
     "@growi/core" "link:packages/core"
     "@growi/core" "link:packages/core"
     "@growi/remark-growi-directive" "link:packages/remark-growi-directive"
     "@growi/remark-growi-directive" "link:packages/remark-growi-directive"
@@ -2570,7 +2570,7 @@
     swr "^2.0.3"
     swr "^2.0.3"
 
 
 "@growi/slack@link:packages/slack":
 "@growi/slack@link:packages/slack":
-  version "6.2.0-RC.0"
+  version "6.2.1-RC.0"
   dependencies:
   dependencies:
     "@slack/oauth" "^2.0.1"
     "@slack/oauth" "^2.0.1"
     "@slack/web-api" "^6.2.4"
     "@slack/web-api" "^6.2.4"
@@ -2586,7 +2586,7 @@
     url-join "^4.0.0"
     url-join "^4.0.0"
 
 
 "@growi/ui@link:packages/ui":
 "@growi/ui@link:packages/ui":
-  version "6.2.0-RC.0"
+  version "6.2.1-RC.0"
   dependencies:
   dependencies:
     "@growi/core" "link:packages/core"
     "@growi/core" "link:packages/core"