Răsfoiți Sursa

Merge branch 'dev/7.0.x' into feat/126520-emoji-features-1

WNomunomu 2 ani în urmă
părinte
comite
c216837782
100 a modificat fișierele cu 796 adăugiri și 676 ștergeri
  1. 1 1
      .github/ISSUE_TEMPLATE/config.yml
  2. 55 1
      CHANGELOG.md
  3. 3 4
      README.md
  4. 3 4
      README_JP.md
  5. 1 1
      SECURITY.md
  6. 1 0
      _obsolete/packages/.eslintignore
  7. 0 0
      _obsolete/packages/hackmd/.eslintignore
  8. 0 0
      _obsolete/packages/hackmd/.gitignore
  9. 0 0
      _obsolete/packages/hackmd/package.json
  10. 0 0
      _obsolete/packages/hackmd/src/hackmd-agent.js
  11. 0 0
      _obsolete/packages/hackmd/src/hackmd-styles.ts
  12. 0 0
      _obsolete/packages/hackmd/src/index.ts
  13. 0 0
      _obsolete/packages/hackmd/src/style.scss
  14. 0 0
      _obsolete/packages/hackmd/tsconfig.json
  15. 0 0
      _obsolete/packages/hackmd/vite.config.js
  16. 0 2
      apps/app/.env.development
  17. 0 0
      apps/app/_obsolete/src/client/services/side-effects/hackmd-draft-updated.ts
  18. 0 0
      apps/app/_obsolete/src/components/PageEditorByHackmd.tsx
  19. 0 0
      apps/app/_obsolete/src/components/PageEditorByHackmd/HackmdEditor.jsx
  20. 0 0
      apps/app/_obsolete/src/interfaces/hackmd.ts
  21. 0 0
      apps/app/_obsolete/src/server/routes/hackmd.js
  22. 0 0
      apps/app/_obsolete/src/stores/hackmd.ts
  23. 0 0
      apps/app/_obsolete/src/styles/theme/_apply-colors-dark.scss
  24. 0 0
      apps/app/_obsolete/src/styles/theme/_apply-colors-light.scss
  25. 0 0
      apps/app/_obsolete/src/styles/theme/_hsl-functions.scss
  26. 0 0
      apps/app/_obsolete/src/styles/theme/_hsl-reboot-bootstrap-theme-colors.scss
  27. 0 0
      apps/app/_obsolete/src/styles/theme/_reboot-bootstrap-border-colors.scss
  28. 0 0
      apps/app/_obsolete/src/styles/theme/_reboot-bootstrap-buttons.scss
  29. 0 0
      apps/app/_obsolete/src/styles/theme/_reboot-bootstrap-colors.scss
  30. 0 0
      apps/app/_obsolete/src/styles/theme/_reboot-bootstrap-dropdown.scss
  31. 0 0
      apps/app/_obsolete/src/styles/theme/_reboot-bootstrap-nav.scss
  32. 0 0
      apps/app/_obsolete/src/styles/theme/_reboot-bootstrap-tables.scss
  33. 0 0
      apps/app/_obsolete/src/styles/theme/_reboot-bootstrap-text.scss
  34. 0 0
      apps/app/_obsolete/src/styles/theme/_reboot-bootstrap-theme-colors.scss
  35. 0 0
      apps/app/_obsolete/src/styles/theme/_reboot-toastr-colors.scss
  36. 0 0
      apps/app/_obsolete/src/styles/theme/apply-colors.scss
  37. 0 0
      apps/app/_obsolete/src/styles/theme/mixins/_count-badge.scss
  38. 0 0
      apps/app/_obsolete/src/styles/theme/mixins/_hsl-badge.scss
  39. 0 0
      apps/app/_obsolete/src/styles/theme/mixins/_hsl-button.scss
  40. 0 0
      apps/app/_obsolete/src/styles/theme/mixins/_list-group.scss
  41. 0 0
      apps/app/_obsolete/src/styles/theme/mixins/_page-editor-mode-manager.scss
  42. 0 2
      apps/app/package.json
  43. 5 5
      apps/app/public/static/locales/en_US/admin.json
  44. 2 22
      apps/app/public/static/locales/en_US/translation.json
  45. 7 6
      apps/app/public/static/locales/ja_JP/admin.json
  46. 2 22
      apps/app/public/static/locales/ja_JP/translation.json
  47. 5 5
      apps/app/public/static/locales/zh_CN/admin.json
  48. 2 22
      apps/app/public/static/locales/zh_CN/translation.json
  49. 1 1
      apps/app/resource/locales/en_US/welcome.md
  50. 1 1
      apps/app/resource/locales/ja_JP/welcome.md
  51. 1 1
      apps/app/resource/locales/zh_CN/welcome.md
  52. 21 2
      apps/app/src/client/services/AdminCustomizeContainer.js
  53. 0 19
      apps/app/src/client/services/page-operation.ts
  54. 58 0
      apps/app/src/client/services/search-operation.ts
  55. 2 4
      apps/app/src/client/services/side-effects/page-updated.ts
  56. 13 15
      apps/app/src/components/Admin/Common/Accordion.jsx
  57. 4 0
      apps/app/src/components/Admin/Customize/Customize.jsx
  58. 0 28
      apps/app/src/components/Admin/Customize/CustomizeFunctionSetting.tsx
  59. 70 0
      apps/app/src/components/Admin/Customize/CustomizePresentationSetting.tsx
  60. 0 13
      apps/app/src/components/Admin/ImportData/GrowiArchive/ImportCollectionConfigurationModal.jsx
  61. 14 18
      apps/app/src/components/Admin/Security/SamlSecuritySettingContents.jsx
  62. 6 6
      apps/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx
  63. 12 12
      apps/app/src/components/Admin/SlackIntegration/WithProxyAccordions.jsx
  64. 1 2
      apps/app/src/components/Bookmarks/BookmarkFolderItem.tsx
  65. 4 2
      apps/app/src/components/Bookmarks/BookmarkFolderTree.module.scss
  66. 0 15
      apps/app/src/components/Icons/TriangleIcon.tsx
  67. 1 1
      apps/app/src/components/InAppNotification/InAppNotificationDropdown.tsx
  68. 4 5
      apps/app/src/components/Navbar/GlobalSearch.tsx
  69. 3 3
      apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  70. 0 127
      apps/app/src/components/Navbar/PageEditorModeManager.jsx
  71. 50 24
      apps/app/src/components/Navbar/PageEditorModeManager.module.scss
  72. 103 0
      apps/app/src/components/Navbar/PageEditorModeManager.tsx
  73. 0 2
      apps/app/src/components/Page/DisplaySwitcher.tsx
  74. 10 1
      apps/app/src/components/Page/RenderTagLabels.tsx
  75. 3 4
      apps/app/src/components/PageEditor/EditorNavbarBottom.tsx
  76. 4 1
      apps/app/src/components/PageEditor/PageEditor.tsx
  77. 0 1
      apps/app/src/components/PagePresentationModal.module.scss
  78. 19 61
      apps/app/src/components/PageStatusAlert.tsx
  79. 3 1
      apps/app/src/components/ReactMarkdownComponents/LightBox.tsx
  80. 15 40
      apps/app/src/components/SearchPage.tsx
  81. 32 20
      apps/app/src/components/SearchPage/SearchControl.tsx
  82. 3 2
      apps/app/src/components/Sidebar/Custom/CustomSidebar.tsx
  83. 6 1
      apps/app/src/components/Sidebar/Custom/CustomSidebarNotFound.tsx
  84. 1 1
      apps/app/src/components/Sidebar/Custom/CustomSidebarSubstance.tsx
  85. 26 0
      apps/app/src/components/Sidebar/PageTree/Item.module.scss
  86. 7 3
      apps/app/src/components/Sidebar/PageTree/Item.tsx
  87. 11 2
      apps/app/src/components/Sidebar/PageTree/ItemsTree.module.scss
  88. 46 46
      apps/app/src/components/Sidebar/PersonalDropdown.tsx
  89. 11 8
      apps/app/src/components/Sidebar/RecentChanges/RecentChangesSubstance.tsx
  90. 28 5
      apps/app/src/components/Sidebar/Sidebar.module.scss
  91. 14 0
      apps/app/src/components/Sidebar/SidebarContents.module.scss
  92. 6 1
      apps/app/src/components/Sidebar/SidebarContents.tsx
  93. 68 29
      apps/app/src/components/Sidebar/SidebarNav.module.scss
  94. 6 12
      apps/app/src/components/Sidebar/SidebarNav.tsx
  95. 7 9
      apps/app/src/components/TagCloudBox.tsx
  96. 12 15
      apps/app/src/components/TagList.tsx
  97. 0 1
      apps/app/src/interfaces/page-operation.ts
  98. 0 3
      apps/app/src/interfaces/websocket.ts
  99. 0 1
      apps/app/src/models/admin/import-option-for-pages.js
  100. 2 10
      apps/app/src/pages/[[...path]].page.tsx

+ 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

+ 3 - 4
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">
@@ -38,8 +38,7 @@
 
 
 - **Features**
 - **Features**
   - Create hierarchical pages with markdown -> [Try GROWI on the demo site](https://docs.growi.org/en/guide/getting-started/try_growi.html)
   - Create hierarchical pages with markdown -> [Try GROWI on the demo site](https://docs.growi.org/en/guide/getting-started/try_growi.html)
-  - Simultaneously edit with multiple people by [HackMD(CodiMD)](https://hackmd.io/) integration
-    - [GROWI Docs: HackMD(CodiMD) Integration](https://docs.growi.org/en/admin-guide/admin-cookbook/integrate-with-hackmd.html)
+  - Simultaneously edit with multiple people
   - Support Authentication with LDAP / Active Directory, OAuth
   - Support Authentication with LDAP / Active Directory, OAuth
   - SSO(Single Sign On) with SAML
   - SSO(Single Sign On) with SAML
   - Slack/Mattermost, IFTTT Integration
   - Slack/Mattermost, IFTTT Integration
@@ -132,7 +131,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
 
 

+ 3 - 4
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">
@@ -37,8 +37,7 @@
 
 
 - **主な機能**
 - **主な機能**
   - マークダウンを使用してページを階層構造で作成することが可能です。 -> [デモサイトで GROWI を体験する](https://docs.growi.org/ja/guide/getting-started/try_growi.html)。
   - マークダウンを使用してページを階層構造で作成することが可能です。 -> [デモサイトで GROWI を体験する](https://docs.growi.org/ja/guide/getting-started/try_growi.html)。
-  - [HackMD(CodiMd)](https://hackmd.io/) と連携することで同時多人数編集が可能です。
-    - [GROWI Docs: HackMD(CodiMD) 連携](https://docs.growi.org/ja/admin-guide/admin-cookbook/integrate-with-hackmd.html)
+  - 同時多人数編集が可能です。
   - LDAP / Active Direcotry , OAuth 認証をサポートしています。
   - LDAP / Active Direcotry , OAuth 認証をサポートしています。
   - SAML を用いた Single Sign On が可能です。
   - SAML を用いた Single Sign On が可能です。
   - Slack / Mattermost, IFTTT と連携することが可能です。
   - Slack / Mattermost, IFTTT と連携することが可能です。
@@ -129,7 +128,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 - 0
_obsolete/packages/.eslintignore

@@ -0,0 +1 @@
+**/*

+ 0 - 0
packages/hackmd/.eslintignore → _obsolete/packages/hackmd/.eslintignore


+ 0 - 0
packages/hackmd/.gitignore → _obsolete/packages/hackmd/.gitignore


+ 0 - 0
packages/hackmd/package.json → _obsolete/packages/hackmd/package.json


+ 0 - 0
packages/hackmd/src/hackmd-agent.js → _obsolete/packages/hackmd/src/hackmd-agent.js


+ 0 - 0
packages/hackmd/src/hackmd-styles.ts → _obsolete/packages/hackmd/src/hackmd-styles.ts


+ 0 - 0
packages/hackmd/src/index.ts → _obsolete/packages/hackmd/src/index.ts


+ 0 - 0
packages/hackmd/src/style.scss → _obsolete/packages/hackmd/src/style.scss


+ 0 - 0
packages/hackmd/tsconfig.json → _obsolete/packages/hackmd/tsconfig.json


+ 0 - 0
packages/hackmd/vite.config.js → _obsolete/packages/hackmd/vite.config.js


+ 0 - 2
apps/app/.env.development

@@ -13,8 +13,6 @@ MONGO_URI="mongodb://mongo:27017/growi"
 ELASTICSEARCH_URI="http://elasticsearch:9200/growi"
 ELASTICSEARCH_URI="http://elasticsearch:9200/growi"
 ELASTICSEARCH_REQUEST_TIMEOUT=15000
 ELASTICSEARCH_REQUEST_TIMEOUT=15000
 ELASTICSEARCH_REJECT_UNAUTHORIZED=true
 ELASTICSEARCH_REJECT_UNAUTHORIZED=true
-HACKMD_URI="http://localhost:3010"
-HACKMD_URI_FOR_SERVER="http://hackmd:3000"
 OGP_URI="http://ogp:8088"
 OGP_URI="http://ogp:8088"
 QUESTIONNAIRE_SERVER_ORIGIN="http://host.docker.internal:3003"
 QUESTIONNAIRE_SERVER_ORIGIN="http://host.docker.internal:3003"
 # DRAWIO_URI="http://localhost:8080/?offline=1&https=0"
 # DRAWIO_URI="http://localhost:8080/?offline=1&https=0"

+ 0 - 0
apps/app/src/client/services/side-effects/hackmd-draft-updated.ts → apps/app/_obsolete/src/client/services/side-effects/hackmd-draft-updated.ts


+ 0 - 0
apps/app/src/components/PageEditorByHackmd.tsx → apps/app/_obsolete/src/components/PageEditorByHackmd.tsx


+ 0 - 0
apps/app/src/components/PageEditorByHackmd/HackmdEditor.jsx → apps/app/_obsolete/src/components/PageEditorByHackmd/HackmdEditor.jsx


+ 0 - 0
apps/app/src/interfaces/hackmd.ts → apps/app/_obsolete/src/interfaces/hackmd.ts


+ 0 - 0
apps/app/src/server/routes/hackmd.js → apps/app/_obsolete/src/server/routes/hackmd.js


+ 0 - 0
apps/app/src/stores/hackmd.ts → apps/app/_obsolete/src/stores/hackmd.ts


+ 0 - 0
apps/app/src/styles/theme/_apply-colors-dark.scss → apps/app/_obsolete/src/styles/theme/_apply-colors-dark.scss


+ 0 - 0
apps/app/src/styles/theme/_apply-colors-light.scss → apps/app/_obsolete/src/styles/theme/_apply-colors-light.scss


+ 0 - 0
apps/app/src/styles/theme/_hsl-functions.scss → apps/app/_obsolete/src/styles/theme/_hsl-functions.scss


+ 0 - 0
apps/app/src/styles/theme/_hsl-reboot-bootstrap-theme-colors.scss → apps/app/_obsolete/src/styles/theme/_hsl-reboot-bootstrap-theme-colors.scss


+ 0 - 0
apps/app/src/styles/theme/_reboot-bootstrap-border-colors.scss → apps/app/_obsolete/src/styles/theme/_reboot-bootstrap-border-colors.scss


+ 0 - 0
apps/app/src/styles/theme/_reboot-bootstrap-buttons.scss → apps/app/_obsolete/src/styles/theme/_reboot-bootstrap-buttons.scss


+ 0 - 0
apps/app/src/styles/theme/_reboot-bootstrap-colors.scss → apps/app/_obsolete/src/styles/theme/_reboot-bootstrap-colors.scss


+ 0 - 0
apps/app/src/styles/theme/_reboot-bootstrap-dropdown.scss → apps/app/_obsolete/src/styles/theme/_reboot-bootstrap-dropdown.scss


+ 0 - 0
apps/app/src/styles/theme/_reboot-bootstrap-nav.scss → apps/app/_obsolete/src/styles/theme/_reboot-bootstrap-nav.scss


+ 0 - 0
apps/app/src/styles/theme/_reboot-bootstrap-tables.scss → apps/app/_obsolete/src/styles/theme/_reboot-bootstrap-tables.scss


+ 0 - 0
apps/app/src/styles/theme/_reboot-bootstrap-text.scss → apps/app/_obsolete/src/styles/theme/_reboot-bootstrap-text.scss


+ 0 - 0
apps/app/src/styles/theme/_reboot-bootstrap-theme-colors.scss → apps/app/_obsolete/src/styles/theme/_reboot-bootstrap-theme-colors.scss


+ 0 - 0
apps/app/src/styles/theme/_reboot-toastr-colors.scss → apps/app/_obsolete/src/styles/theme/_reboot-toastr-colors.scss


+ 0 - 0
apps/app/src/styles/theme/apply-colors.scss → apps/app/_obsolete/src/styles/theme/apply-colors.scss


+ 0 - 0
apps/app/src/styles/theme/mixins/_count-badge.scss → apps/app/_obsolete/src/styles/theme/mixins/_count-badge.scss


+ 0 - 0
apps/app/src/styles/theme/mixins/_hsl-badge.scss → apps/app/_obsolete/src/styles/theme/mixins/_hsl-badge.scss


+ 0 - 0
apps/app/src/styles/theme/mixins/_hsl-button.scss → apps/app/_obsolete/src/styles/theme/mixins/_hsl-button.scss


+ 0 - 0
apps/app/src/styles/theme/mixins/_list-group.scss → apps/app/_obsolete/src/styles/theme/mixins/_list-group.scss


+ 0 - 0
apps/app/src/styles/theme/mixins/_page-editor-mode-manager.scss → apps/app/_obsolete/src/styles/theme/mixins/_page-editor-mode-manager.scss


+ 0 - 2
apps/app/package.json

@@ -65,7 +65,6 @@
     "@godaddy/terminus": "^4.9.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
     "@google-cloud/storage": "^5.8.5",
     "@growi/core": "link:../../packages/core",
     "@growi/core": "link:../../packages/core",
-    "@growi/hackmd": "link:../../packages/hackmd",
     "@growi/pluginkit": "link:../../packages/pluginkit",
     "@growi/pluginkit": "link:../../packages/pluginkit",
     "@growi/preset-templates": "link:../../packages/preset-templates",
     "@growi/preset-templates": "link:../../packages/preset-templates",
     "@growi/preset-themes": "link:../../packages/preset-themes",
     "@growi/preset-themes": "link:../../packages/preset-themes",
@@ -246,7 +245,6 @@
     "mongodb-memory-server": "^8.12.2",
     "mongodb-memory-server": "^8.12.2",
     "morgan": "^1.10.0",
     "morgan": "^1.10.0",
     "null-loader": "^4.0.1",
     "null-loader": "^4.0.1",
-    "penpal": "^4.0.0",
     "plantuml-encoder": "^1.2.5",
     "plantuml-encoder": "^1.2.5",
     "prettier": "^1.19.1",
     "prettier": "^1.19.1",
     "react-codemirror2": "^6.0.0",
     "react-codemirror2": "^6.0.0",

+ 5 - 5
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",
@@ -553,10 +557,6 @@
           "initialize_meta_datas": {
           "initialize_meta_datas": {
             "label": "Initialize page's like, read users and comment count",
             "label": "Initialize page's like, read users and comment count",
             "desc": "Recommended <span class=\"text-danger\">NOT</span> to check this when users will also be restored."
             "desc": "Recommended <span class=\"text-danger\">NOT</span> to check this when users will also be restored."
-          },
-          "initialize_hackmd_related_datas": {
-            "label": "Initialize HackMD related data",
-            "desc": "Recommended to check this unless there is important drafts on HackMD."
           }
           }
         },
         },
         "revisions": {
         "revisions": {

+ 2 - 22
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",
@@ -501,27 +502,6 @@
     "insert_image": "inserts an image",
     "insert_image": "inserts an image",
     "open_sandbox": "Open Sandbox"
     "open_sandbox": "Open Sandbox"
   },
   },
-  "hackmd": {
-    "hack_md": "HackMD",
-    "not_set_up": "HackMD is not set up.",
-    "used_for_not_found": "Can not use HackMD to a page that does not exist.",
-    "start_to_edit": "Start to edit with HackMD",
-    "clone_page_content": "Click to clone page content and start to edit.",
-    "unsaved_draft": "HackMD has unsaved draft.",
-    "draft_outdated": "DRAFT MAY BE OUTDATED",
-    "based_on_revision": "The current draft on HackMD is based on",
-    "view_outdated_draft": "View the outdated draft on HackMD",
-    "resume_to_edit": "Resume to edit with HackMD",
-    "discard_changes": "Discard changes of HackMD",
-    "integration_failed": "HackMD Integration failed",
-    "fail_to_connect": "GROWI client failed to connect to GROWI agent for HackMD.",
-    "check_configuration": "Check your configuration following <a href='https://docs.growi.org/en/admin-guide/admin-cookbook/integrate-with-hackmd.html'>the manual</a>.",
-    "not_initialized": "HackmdEditor component has not initialized",
-    "someone_editing": "Someone editing this page on HackMD",
-    "this_page_has_draft": "This page has a draft on HackMD",
-    "need_to_associate_with_growi_to_use_hackmd_refer_to_this": "To use HackMD for simultaneous multi-person editing, need to associate HackMD with GROWI.Please refer to <a href='https://docs.growi.org/en/admin-guide/admin-cookbook/integrate-with-hackmd.html'>here</a>.",
-    "need_to_make_page": "To use HackMD, please make a new page from the <a href='#edit'>built-in editor.</a>"
-  },
   "slack_notification": {
   "slack_notification": {
     "popover_title": "Slack Notification",
     "popover_title": "Slack Notification",
     "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."
@@ -736,7 +716,7 @@
         "isForbidden": "Authority not allowed to view",
         "isForbidden": "Authority not allowed to view",
         "currentPageGrantLabel": "Authorization for this page: ",
         "currentPageGrantLabel": "Authorization for this page: ",
         "parentPageGrantLabel": "Authority of parent page: ",
         "parentPageGrantLabel": "Authority of parent page: ",
-        "docLink": "For more information on modifying permissions, please refer to <a href='https://docs.growi.org/ja/admin-guide/admin-cookbook/integrate-with-hackmd.html'>こちらのリンク</a>"
+        "docLink": "For more information on modifying permissions, please refer to <a href='https://docs.growi.org/en/guide/features/authority.html#permissions-for-subordinate-pages'>こちらのリンク</a>"
       },
       },
       "radio_btn": {
       "radio_btn": {
         "restrected": "Only those who know the link",
         "restrected": "Only those who know the link",

+ 7 - 6
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": "記事",
@@ -561,10 +566,6 @@
           "initialize_meta_datas": {
           "initialize_meta_datas": {
             "label": "「いいね」「閲覧したユーザー」「コメント数」を初期化する",
             "label": "「いいね」「閲覧したユーザー」「コメント数」を初期化する",
             "desc": "users を同時に復元しない場合、このオプションは<span class=\"text-danger\">非推奨</span>です。"
             "desc": "users を同時に復元しない場合、このオプションは<span class=\"text-danger\">非推奨</span>です。"
-          },
-          "initialize_hackmd_related_datas": {
-            "label": "HackMD 関連データを初期化する",
-            "desc": "HackMD に重要な下書きデータがない限りはこのオプションをチェックすることを推奨します。"
           }
           }
         },
         },
         "revisions": {
         "revisions": {

+ 2 - 22
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": "添付ファイルも含める",
@@ -534,27 +535,6 @@
     "insert_image": "で画像を挿入できます",
     "insert_image": "で画像を挿入できます",
     "open_sandbox": "Sandbox を開く"
     "open_sandbox": "Sandbox を開く"
   },
   },
-  "hackmd":{
-    "hack_md": "HackMD",
-    "not_set_up": "HackMD はセットアップされていません",
-    "used_for_not_found": "HackMD は新しいページの作成には利用できません",
-    "start_to_edit": "HackMD を開始する",
-    "clone_page_content": "ページを複製して編集を開始します",
-    "unsaved_draft": "HackMD のドラフトが保存されていません",
-    "draft_outdated": "ドラフトは古くなっている可能性があります",
-    "based_on_revision": "現在のドラフトは次の revision に基づいています",
-    "view_outdated_draft": "HackMD で古いドラフトを表示する",
-    "resume_to_edit": "HackMD で編集を再開する",
-    "discard_changes": "HackMD の変更を破棄する",
-    "integration_failed": "HackMD の統合に失敗しました",
-    "fail_to_connect": "GROWI クライアントが HackMD の GROWI agent に接続できませんでした。",
-    "check_configuration": "<a href='https://docs.growi.org/ja/admin-guide/admin-cookbook/integrate-with-hackmd.html'>こちらのマニュアル</a>から設定を確認してください",
-    "not_initialized": "HackMD コンポーネントは初期化されていません",
-    "someone_editing": "このページは、HackMD で編集されています。",
-    "this_page_has_draft": "このページは、HackMD のドラフトがあります。",
-    "need_to_associate_with_growi_to_use_hackmd_refer_to_this": "HackMD を利用して同時多人数編集を行うには、HackMD と GROWI を連携する必要があります。<a href='https://docs.growi.org/ja/admin-guide/admin-cookbook/integrate-with-hackmd.html'>こちら</a>を参照してください。",
-    "need_to_make_page": "HackMD を利用するためには、<a href='#edit'>ビルトインエディタ</a>で新しいページを作成してください。"
-  },
   "slack_notification": {
   "slack_notification": {
     "popover_title": "Slack 通知",
     "popover_title": "Slack 通知",
     "popover_desc": "チャンネル名を入れてください。カンマ区切りのリストを入力することで複数のチャンネルに通知することができます。"
     "popover_desc": "チャンネル名を入れてください。カンマ区切りのリストを入力することで複数のチャンネルに通知することができます。"
@@ -769,7 +749,7 @@
         "isForbidden": "権限の閲覧が許可されていません",
         "isForbidden": "権限の閲覧が許可されていません",
         "currentPageGrantLabel": "このページの権限: ",
         "currentPageGrantLabel": "このページの権限: ",
         "parentPageGrantLabel": "親のページの権限: ",
         "parentPageGrantLabel": "親のページの権限: ",
-        "docLink": "権限の修正についての詳細は<a href='https://docs.growi.org/ja/admin-guide/admin-cookbook/integrate-with-hackmd.html'>こちらのリンク</a>を参照してください"
+        "docLink": "権限の修正についての詳細は<a href='https://docs.growi.org/ja/guide/features/authority.html#%E9%85%8D%E4%B8%8B%E3%83%98%E3%82%9A%E3%83%BC%E3%82%B7%E3%82%99%E3%81%AB%E8%A8%AD%E5%AE%9A%E3%81%A6%E3%82%99%E3%81%8D%E3%82%8B%E6%A8%A9%E9%99%90'>こちらのリンク</a>を参照してください"
       },
       },
       "radio_btn": {
       "radio_btn": {
         "restrected": "リンクを知っている人のみ",
         "restrected": "リンクを知っている人のみ",

+ 5 - 5
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": "自定义徽标",
@@ -561,10 +565,6 @@
           "initialize_meta_datas": {
           "initialize_meta_datas": {
             "label": "Initialize page's like, read users and comment count",
             "label": "Initialize page's like, read users and comment count",
             "desc": "Recommended <span class=\"text-danger\">NOT</span> to check this when users will also be restored."
             "desc": "Recommended <span class=\"text-danger\">NOT</span> to check this when users will also be restored."
-          },
-          "initialize_hackmd_related_datas": {
-            "label": "Initialize HackMD related data",
-            "desc": "Recommended to check this unless there is important drafts on HackMD."
           }
           }
         },
         },
         "revisions": {
         "revisions": {

+ 2 - 22
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": "包含附件",
@@ -488,27 +489,6 @@
 		"insert_image": "插入图像",
 		"insert_image": "插入图像",
 		"open_sandbox": "开放式沙箱"
 		"open_sandbox": "开放式沙箱"
 	},
 	},
-	"hackmd": {
-    "hack_md": "HackMD",
-    "not_set_up": "HackMD is not set up.",
-    "used_for_not_found": "Can not use HackMD to a page that does not exist.",
-		"start_to_edit": "Start to edit with HackMD",
-		"clone_page_content": "Click to clone page content and start to edit.",
-		"unsaved_draft": "HackMD has unsaved draft.",
-		"draft_outdated": "DRAFT MAY BE OUTDATED",
-		"based_on_revision": "The current draft on HackMD is based on",
-		"view_outdated_draft": "View the outdated draft on HackMD",
-		"resume_to_edit": "Resume to edit with HackMD",
-		"discard_changes": "Discard changes of HackMD",
-		"integration_failed": "HackMD Integration failed",
-		"fail_to_connect": "GROWI client failed to connect to GROWI agent for HackMD.",
-		"check_configuration": "Check your configuration following <a href='https://docs.growi.org/en/admin-guide/admin-cookbook/integrate-with-hackmd.html'>the manual</a>.",
-		"not_initialized": "HackmdEditor component has not initialized",
-		"someone_editing": "Someone editing this page on HackMD",
-    "this_page_has_draft": "This page has a draft on HackMD",
-    "need_to_associate_with_growi_to_use_hackmd_refer_to_this": "若要使用HackMD的多人同时编辑功能,请先关联HackMD和GROWI。详情请参考<a href='https://docs.growi.org/en/admin-guide/admin-cookbook/integrate-with-hackmd.html'>这里</a>。",
-    "need_to_make_page": "To use HackMD, please make a new page from the <a href='#edit'>built-in editor.</a>"
-  },
   "slack_notification": {
   "slack_notification": {
     "popover_title": "Slack Notification",
     "popover_title": "Slack Notification",
     "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."
@@ -739,7 +719,7 @@
         "isForbidden": "无权查看的机构",
         "isForbidden": "无权查看的机构",
         "currentPageGrantLabel": "本页的权限: ",
         "currentPageGrantLabel": "本页的权限: ",
         "parentPageGrantLabel": "父页的权限: ",
         "parentPageGrantLabel": "父页的权限: ",
-        "docLink": "关于修改授权的更多信息,请参见此<a href='https://docs.growi.org/ja/admin-guide/admin-cookbook/integrate-with-hackmd.html'>此链接</a>"
+        "docLink": "关于修改授权的更多信息,请参见此<a href='https://docs.growi.org/en/guide/features/authority.html#permissions-for-subordinate-pages'>此链接</a>"
       },
       },
       "radio_btn": {
       "radio_btn": {
         "restrected": "只有那些知道链接的人",
         "restrected": "只有那些知道链接的人",

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

+ 0 - 19
apps/app/src/client/services/page-operation.ts

@@ -135,23 +135,6 @@ export const useSaveOrUpdate = (): SaveOrUpdateFunction => {
     const { path, pageId, revisionId } = pageInfo;
     const { path, pageId, revisionId } = pageInfo;
 
 
     const options: OptionsToSave = Object.assign({}, optionsToSave);
     const options: OptionsToSave = Object.assign({}, optionsToSave);
-    /*
-    * Note: variable "markdown" will be received from params
-    * please delete the following code after implemating HackMD editor function
-    */
-    // let markdown;
-    // if (editorMode === EditorMode.HackMD) {
-    // const pageEditorByHackmd = this.appContainer.getComponentInstance('PageEditorByHackmd');
-    // markdown = await pageEditorByHackmd.getMarkdown();
-    // // set option to sync
-    // options.isSyncRevisionToHackmd = true;
-    // revisionId = this.state.revisionIdHackmdSynced;
-    // }
-    // else {
-    // const pageEditor = this.appContainer.getComponentInstance('PageEditor');
-    // const pageEditor = getComponentInstance('PageEditor');
-    // markdown = pageEditor.getMarkdown();
-    // }
 
 
     let res;
     let res;
     if (pageId == null || revisionId == null) {
     if (pageId == null || revisionId == null) {
@@ -209,8 +192,6 @@ export const useUpdateStateAfterSave = (pageId: string|undefined|null, opts?: Up
       remoteRevisionBody: updatedPage.revision.body,
       remoteRevisionBody: updatedPage.revision.body,
       remoteRevisionLastUpdateUser: updatedPage.lastUpdateUser,
       remoteRevisionLastUpdateUser: updatedPage.lastUpdateUser,
       remoteRevisionLastUpdatedAt: updatedPage.updatedAt,
       remoteRevisionLastUpdatedAt: updatedPage.updatedAt,
-      revisionIdHackmdSynced: updatedPage.revisionHackmdSynced?.toString(),
-      hasDraftOnHackmd: updatedPage.hasDraftOnHackmd,
     };
     };
 
 
     setRemoteLatestPageData(remoterevisionData);
     setRemoteLatestPageData(remoterevisionData);

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

+ 2 - 4
apps/app/src/client/services/side-effects/page-updated.ts

@@ -2,7 +2,7 @@ import { useCallback, useEffect } from 'react';
 
 
 import { SocketEventName } from '~/interfaces/websocket';
 import { SocketEventName } from '~/interfaces/websocket';
 import { useCurrentPageId } from '~/stores/page';
 import { useCurrentPageId } from '~/stores/page';
-import { useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
+import { useSetRemoteLatestPageData, type RemoteRevisionData } from '~/stores/remote-latest-page';
 import { useGlobalSocket } from '~/stores/websocket';
 import { useGlobalSocket } from '~/stores/websocket';
 
 
 export const usePageUpdatedEffect = (): void => {
 export const usePageUpdatedEffect = (): void => {
@@ -15,13 +15,11 @@ export const usePageUpdatedEffect = (): void => {
   const setLatestRemotePageData = useCallback((data) => {
   const setLatestRemotePageData = useCallback((data) => {
     const { s2cMessagePageUpdated } = data;
     const { s2cMessagePageUpdated } = data;
 
 
-    const remoteData = {
+    const remoteData: RemoteRevisionData = {
       remoteRevisionId: s2cMessagePageUpdated.revisionId,
       remoteRevisionId: s2cMessagePageUpdated.revisionId,
       remoteRevisionBody: s2cMessagePageUpdated.revisionBody,
       remoteRevisionBody: s2cMessagePageUpdated.revisionBody,
       remoteRevisionLastUpdateUser: s2cMessagePageUpdated.remoteLastUpdateUser,
       remoteRevisionLastUpdateUser: s2cMessagePageUpdated.remoteLastUpdateUser,
       remoteRevisionLastUpdatedAt: s2cMessagePageUpdated.revisionUpdateAt,
       remoteRevisionLastUpdatedAt: s2cMessagePageUpdated.revisionUpdateAt,
-      revisionIdHackmdSynced: s2cMessagePageUpdated.revisionIdHackmdSynced,
-      hasDraftOnHackmd: s2cMessagePageUpdated.hasDraftOnHackmd,
     };
     };
 
 
     if (currentPageId != null && currentPageId === s2cMessagePageUpdated.pageId) {
     if (currentPageId != null && currentPageId === s2cMessagePageUpdated.pageId) {

+ 13 - 15
apps/app/src/components/Admin/Common/Accordion.jsx

@@ -4,25 +4,23 @@ import PropTypes from 'prop-types';
 import { Collapse } from 'reactstrap';
 import { Collapse } from 'reactstrap';
 
 
 
 
-// TODO: use new accordion component
-// https://redmine.weseek.co.jp/issues/129222
 const Accordion = (props) => {
 const Accordion = (props) => {
   const [isOpen, setIsOpen] = useState(props.isOpenDefault);
   const [isOpen, setIsOpen] = useState(props.isOpenDefault);
   return (
   return (
-    <div className="card border-0 rounded-3 mb-0">
-      <div
-        className="card-header fw-normal py-3 d-flex justify-content-between"
-        role="button"
-        onClick={() => setIsOpen(prevState => !prevState)}
-      >
-        <p className="mb-0">{props.title}</p>
-        {isOpen
-          ? <i className="fa fa-chevron-up" />
-          : <i className="fa fa-chevron-down" />
-        }
-      </div>
+    <div className="accordion-item">
+      <p className="accordion-header" id="headingOne">
+        <button
+          className={`accordion-button ${isOpen ? '' : 'collapsed'}`}
+          type="button"
+          data-bs-toggle="collapse"
+          aria-expanded="true"
+          onClick={() => setIsOpen(prevState => !prevState)}
+        >
+          {props.title}
+        </button>
+      </p>
       <Collapse isOpen={isOpen}>
       <Collapse isOpen={isOpen}>
-        <div className="card-body">
+        <div className="accordion-body">
           {props.children}
           {props.children}
         </div>
         </div>
       </Collapse>
       </Collapse>

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

+ 0 - 13
apps/app/src/components/Admin/ImportData/GrowiArchive/ImportCollectionConfigurationModal.jsx

@@ -140,19 +140,6 @@ class ImportCollectionConfigurationModal extends React.Component {
             <p className="form-text text-muted mt-0" dangerouslySetInnerHTML={{ __html: t(`${translationBase}.initialize_meta_datas.desc`) }} />
             <p className="form-text text-muted mt-0" dangerouslySetInnerHTML={{ __html: t(`${translationBase}.initialize_meta_datas.desc`) }} />
           </label>
           </label>
         </div>
         </div>
-        <div className="form-check form-check-warning">
-          <input
-            id="cbOpt6"
-            type="checkbox"
-            className="form-check-input"
-            checked={option.initHackmdDatas || false} // add ' || false' to avoid uncontrolled input warning
-            onChange={() => this.changeHandler({ initHackmdDatas: !option.initHackmdDatas })}
-          />
-          <label htmlFor="cbOpt6" className="form-label form-check-label">
-            {t(`${translationBase}.initialize_hackmd_related_datas.label`)}
-            <p className="form-text text-muted mt-0" dangerouslySetInnerHTML={{ __html: t(`${translationBase}.initialize_hackmd_related_datas.desc`) }} />
-          </label>
-        </div>
       </>
       </>
     );
     );
     /* eslint-enable react/no-unescaped-entities */
     /* eslint-enable react/no-unescaped-entities */

+ 14 - 18
apps/app/src/components/Admin/Security/SamlSecuritySettingContents.jsx

@@ -474,25 +474,21 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                           Apache Lucene - Query Parser Syntax <i className="icon-share-alt"></i>
                           Apache Lucene - Query Parser Syntax <i className="icon-share-alt"></i>
                         </a>.
                         </a>.
                       </p>
                       </p>
-                      {/* TODO: use new accordion component */}
-                      {/* https://redmine.weseek.co.jp/issues/129222 */}
-                      <div className="accordion" id="accordionExample">
-                        <div className="card">
-                          <div className="card-header p-1">
-                            <h2 className="mb-0">
-                              <button
-                                className="btn btn-link text-start"
-                                type="button"
-                                onClick={() => this.setState({ isHelpOpened: !this.state.isHelpOpened })}
-                                aria-expanded="true"
-                                aria-controls="ablchelp"
-                              >
-                                <i className={`icon-fw ${this.state.isHelpOpened ? 'icon-arrow-down' : 'icon-arrow-right'} small`}></i> Show more...
-                              </button>
-                            </h2>
-                          </div>
+                      <div className="accordion" id="accordionId">
+                        <div className="accordion-item p-1">
+                          <h2 className="accordion-header">
+                            <button
+                              className="btn btn-link text-start"
+                              type="button"
+                              onClick={() => this.setState({ isHelpOpened: !this.state.isHelpOpened })}
+                              aria-expanded="true"
+                              aria-controls="ablchelp"
+                            >
+                              <i className={`icon-fw ${this.state.isHelpOpened ? 'icon-arrow-down' : 'icon-arrow-right'} small`}></i> Show more...
+                            </button>
+                          </h2>
                           <Collapse isOpen={this.state.isHelpOpened}>
                           <Collapse isOpen={this.state.isHelpOpened}>
-                            <div className="card-body">
+                            <div className="accordion-body">
                               <p dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.attr_based_login_control_rule_help') }} />
                               <p dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.attr_based_login_control_rule_help') }} />
                               <p dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.attr_based_login_control_rule_example1') }} />
                               <p dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.attr_based_login_control_rule_example1') }} />
                               <p dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.attr_based_login_control_rule_example2') }} />
                               <p dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.attr_based_login_control_rule_example2') }} />

+ 6 - 6
apps/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx

@@ -70,10 +70,10 @@ const CustomBotWithoutProxySettingsAccordion = (props) => {
   );
   );
 
 
   return (
   return (
-    <div className="card border-0 rounded-3 shadow overflow-hidden">
+    <div className="accordion">
       <Accordion
       <Accordion
         defaultIsActive={defaultOpenAccordionKeys.has(botInstallationStep.CREATE_BOT)}
         defaultIsActive={defaultOpenAccordionKeys.has(botInstallationStep.CREATE_BOT)}
-        title={<><span className="me-2">①</span>{t('admin:slack_integration.accordion.create_bot')}</>}
+        title={<><span className="me-3">1</span>{t('admin:slack_integration.accordion.create_bot')}</>}
       >
       >
         <div className="my-5 d-flex flex-column align-items-center">
         <div className="my-5 d-flex flex-column align-items-center">
           <button type="button" className="btn btn-primary text-nowrap" onClick={() => window.open('https://api.slack.com/apps', '_blank')}>
           <button type="button" className="btn btn-primary text-nowrap" onClick={() => window.open('https://api.slack.com/apps', '_blank')}>
@@ -96,7 +96,7 @@ const CustomBotWithoutProxySettingsAccordion = (props) => {
       </Accordion>
       </Accordion>
       <Accordion
       <Accordion
         defaultIsActive={defaultOpenAccordionKeys.has(botInstallationStep.INSTALL_BOT)}
         defaultIsActive={defaultOpenAccordionKeys.has(botInstallationStep.INSTALL_BOT)}
-        title={<><span className="me-2">②</span>{t('admin:slack_integration.accordion.install_bot_to_slack')}</>}
+        title={<><span className="me-3">2</span>{t('admin:slack_integration.accordion.install_bot_to_slack')}</>}
       >
       >
         <div className="container w-75 py-5">
         <div className="container w-75 py-5">
           <p>1. {t('admin:slack_integration.accordion.select_install_your_app')}</p>
           <p>1. {t('admin:slack_integration.accordion.select_install_your_app')}</p>
@@ -115,7 +115,7 @@ const CustomBotWithoutProxySettingsAccordion = (props) => {
       <Accordion
       <Accordion
         defaultIsActive={defaultOpenAccordionKeys.has(botInstallationStep.REGISTER_SLACK_CONFIGURATION)}
         defaultIsActive={defaultOpenAccordionKeys.has(botInstallationStep.REGISTER_SLACK_CONFIGURATION)}
         // eslint-disable-next-line max-len
         // eslint-disable-next-line max-len
-        title={<><span className="me-2">③</span>{t('admin:slack_integration.accordion.register_secret_and_token')}{isEnterdSecretAndToken && <i className="ms-3 text-success fa fa-check"></i>}</>}
+        title={<><span className="me-3">3</span>{t('admin:slack_integration.accordion.register_secret_and_token')}{isEnterdSecretAndToken && <i className="ms-3 text-success fa fa-check"></i>}</>}
       >
       >
         <CustomBotWithoutProxySecretTokenSection
         <CustomBotWithoutProxySecretTokenSection
           onUpdatedSecretToken={props.onUpdatedSecretToken}
           onUpdatedSecretToken={props.onUpdatedSecretToken}
@@ -128,7 +128,7 @@ const CustomBotWithoutProxySettingsAccordion = (props) => {
       <Accordion
       <Accordion
         defaultIsActive={defaultOpenAccordionKeys.has(botInstallationStep.CONNECTION_TEST)}
         defaultIsActive={defaultOpenAccordionKeys.has(botInstallationStep.CONNECTION_TEST)}
         // eslint-disable-next-line max-len
         // eslint-disable-next-line max-len
-        title={<><span className="me-2">④</span>{t('admin:slack_integration.accordion.manage_permission')}</>}
+        title={<><span className="me-3">4</span>{t('admin:slack_integration.accordion.manage_permission')}</>}
       >
       >
         <ManageCommandsProcessWithoutProxy
         <ManageCommandsProcessWithoutProxy
           commandPermission={commandPermission}
           commandPermission={commandPermission}
@@ -138,7 +138,7 @@ const CustomBotWithoutProxySettingsAccordion = (props) => {
       <Accordion
       <Accordion
         defaultIsActive={defaultOpenAccordionKeys.has(botInstallationStep.CONNECTION_TEST)}
         defaultIsActive={defaultOpenAccordionKeys.has(botInstallationStep.CONNECTION_TEST)}
         // eslint-disable-next-line max-len
         // eslint-disable-next-line max-len
-        title={<><span className="me-2">⑤</span>{t('admin:slack_integration.accordion.test_connection')}{isLatestConnectionSuccess && <i className="ms-3 text-success fa fa-check"></i>}</>}
+        title={<><span className="me-3">5</span>{t('admin:slack_integration.accordion.test_connection')}{isLatestConnectionSuccess && <i className="ms-3 text-success fa fa-check"></i>}</>}
       >
       >
         <p className="text-center m-4">{t('admin:slack_integration.accordion.test_connection_by_pressing_button')}</p>
         <p className="text-center m-4">{t('admin:slack_integration.accordion.test_connection_by_pressing_button')}</p>
         <p className="text-center text-warning">
         <p className="text-center text-warning">

+ 12 - 12
apps/app/src/components/Admin/SlackIntegration/WithProxyAccordions.jsx

@@ -301,11 +301,11 @@ const WithProxyAccordions = (props) => {
 
 
 
 
   const officialBotIntegrationProcedure = {
   const officialBotIntegrationProcedure = {
-    '①': {
+    1: {
       title: 'install_bot_to_slack',
       title: 'install_bot_to_slack',
       content: <BotInstallProcessForOfficialBot />,
       content: <BotInstallProcessForOfficialBot />,
     },
     },
-    '②': {
+    2: {
       title: 'register_for_growi_official_bot_proxy_service',
       title: 'register_for_growi_official_bot_proxy_service',
       content: <GeneratingTokensAndRegisteringProxyServiceProcess
       content: <GeneratingTokensAndRegisteringProxyServiceProcess
         growiUrl={siteUrl}
         growiUrl={siteUrl}
@@ -315,7 +315,7 @@ const WithProxyAccordions = (props) => {
         onUpdateTokens={props.onUpdateTokens}
         onUpdateTokens={props.onUpdateTokens}
       />,
       />,
     },
     },
-    '③': {
+    3: {
       title: 'manage_permission',
       title: 'manage_permission',
       content: <ManageCommandsProcess
       content: <ManageCommandsProcess
         slackAppIntegrationId={props.slackAppIntegrationId}
         slackAppIntegrationId={props.slackAppIntegrationId}
@@ -324,7 +324,7 @@ const WithProxyAccordions = (props) => {
         permissionsForSlackEventActions={props.permissionsForSlackEventActions}
         permissionsForSlackEventActions={props.permissionsForSlackEventActions}
       />,
       />,
     },
     },
-    '④': {
+    4: {
       title: 'test_connection',
       title: 'test_connection',
       content: <TestProcess
       content: <TestProcess
         slackAppIntegrationId={props.slackAppIntegrationId}
         slackAppIntegrationId={props.slackAppIntegrationId}
@@ -336,15 +336,15 @@ const WithProxyAccordions = (props) => {
   };
   };
 
 
   const CustomBotIntegrationProcedure = {
   const CustomBotIntegrationProcedure = {
-    '①': {
+    1: {
       title: 'create_bot',
       title: 'create_bot',
       content: <BotCreateProcess />,
       content: <BotCreateProcess />,
     },
     },
-    '②': {
+    2: {
       title: 'install_bot_to_slack',
       title: 'install_bot_to_slack',
       content: <BotInstallProcessForCustomBotWithProxy />,
       content: <BotInstallProcessForCustomBotWithProxy />,
     },
     },
-    '③': {
+    3: {
       title: 'register_for_growi_custom_bot_proxy',
       title: 'register_for_growi_custom_bot_proxy',
       content: <GeneratingTokensAndRegisteringProxyServiceProcess
       content: <GeneratingTokensAndRegisteringProxyServiceProcess
         growiUrl={siteUrl}
         growiUrl={siteUrl}
@@ -354,11 +354,11 @@ const WithProxyAccordions = (props) => {
         onUpdateTokens={props.onUpdateTokens}
         onUpdateTokens={props.onUpdateTokens}
       />,
       />,
     },
     },
-    '④': {
+    4: {
       title: 'set_proxy_url_on_growi',
       title: 'set_proxy_url_on_growi',
       content: <RegisteringProxyUrlProcess />,
       content: <RegisteringProxyUrlProcess />,
     },
     },
-    '⑤': {
+    5: {
       title: 'manage_permission',
       title: 'manage_permission',
       content: <ManageCommandsProcess
       content: <ManageCommandsProcess
         slackAppIntegrationId={props.slackAppIntegrationId}
         slackAppIntegrationId={props.slackAppIntegrationId}
@@ -367,7 +367,7 @@ const WithProxyAccordions = (props) => {
         permissionsForSlackEventActions={props.permissionsForSlackEventActions}
         permissionsForSlackEventActions={props.permissionsForSlackEventActions}
       />,
       />,
     },
     },
-    '⑥': {
+    6: {
       title: 'test_connection',
       title: 'test_connection',
       content: <TestProcess
       content: <TestProcess
         slackAppIntegrationId={props.slackAppIntegrationId}
         slackAppIntegrationId={props.slackAppIntegrationId}
@@ -382,14 +382,14 @@ const WithProxyAccordions = (props) => {
 
 
   return (
   return (
     <div
     <div
-      className="card border-0 rounded-3 shadow overflow-hidden"
+      className="accordion"
     >
     >
       {Object.entries(integrationProcedureMapping).map(([key, value]) => {
       {Object.entries(integrationProcedureMapping).map(([key, value]) => {
         return (
         return (
           <Accordion
           <Accordion
             title={(
             title={(
               <>
               <>
-                <span className="me-2">{key}</span>
+                <span className="me-3">{key}</span>
                 {t(`admin:slack_integration.accordion.${value.title}`)}
                 {t(`admin:slack_integration.accordion.${value.title}`)}
                 {value.title === 'test_connection' && isLatestConnectionSuccess && <i className="ms-3 text-success fa fa-check"></i>}
                 {value.title === 'test_connection' && isLatestConnectionSuccess && <i className="ms-3 text-success fa fa-check"></i>}
               </>
               </>

+ 1 - 2
apps/app/src/components/Bookmarks/BookmarkFolderItem.tsx

@@ -10,7 +10,6 @@ import {
 } from '~/client/util/bookmark-utils';
 } from '~/client/util/bookmark-utils';
 import { toastError } from '~/client/util/toastr';
 import { toastError } from '~/client/util/toastr';
 import { FolderIcon } from '~/components/Icons/FolderIcon';
 import { FolderIcon } from '~/components/Icons/FolderIcon';
-import { TriangleIcon } from '~/components/Icons/TriangleIcon';
 import {
 import {
   BookmarkFolderItems, DragItemDataType, DragItemType, DRAG_ITEM_TYPE,
   BookmarkFolderItems, DragItemDataType, DragItemType, DRAG_ITEM_TYPE,
 } from '~/interfaces/bookmark-info';
 } from '~/interfaces/bookmark-info';
@@ -234,7 +233,7 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
                 onClick={loadChildFolder}
                 onClick={loadChildFolder}
               >
               >
                 <div className="d-flex justify-content-center">
                 <div className="d-flex justify-content-center">
-                  <TriangleIcon />
+                  <span className="material-icons-round">arrow_right</span>
                 </div>
                 </div>
               </button>
               </button>
             )}
             )}

+ 4 - 2
apps/app/src/components/Bookmarks/BookmarkFolderTree.module.scss

@@ -40,7 +40,7 @@ $grw-bookmark-item-padding-left: 35px;
     }
     }
 
 
     .grw-foldertree-triangle-btn {
     .grw-foldertree-triangle-btn {
-      background-color: transparent;
+      border: 0;
       transition: all 0.2s ease-out;
       transition: all 0.2s ease-out;
       transform: rotate(0deg);
       transform: rotate(0deg);
 
 
@@ -58,7 +58,9 @@ $grw-bookmark-item-padding-left: 35px;
 
 
   .grw-foldertree-item-container {
   .grw-foldertree-item-container {
     .grw-triangle-container {
     .grw-triangle-container {
-      min-width: 35px;
+      // TODO: ignore width frickering
+      // https://redmine.weseek.co.jp/issues/130828
+      // min-width: 35px;
       height: 40px;
       height: 40px;
     }
     }
 
 

+ 0 - 15
apps/app/src/components/Icons/TriangleIcon.tsx

@@ -1,15 +0,0 @@
-import React from 'react';
-
-export const TriangleIcon = (): JSX.Element => (
-  <svg
-    xmlns="http://www.w3.org/2000/svg"
-    width="12"
-    height="12"
-    viewBox="0 0 12 12"
-  >
-    <g transform="translate(18194 -6790)">
-      <rect width="12" height="12" transform="translate(-18194 6790)" fill="none" />
-      <path d="M5.2,1.067a1,1,0,0,1,1.6,0l4,5.333A1,1,0,0,1,10,8H2a1,1,0,0,1-.8-1.6Z" transform="translate(-18183 6790) rotate(90)" />
-    </g>
-  </svg>
-);

+ 1 - 1
apps/app/src/components/InAppNotification/InAppNotificationDropdown.tsx

@@ -83,7 +83,7 @@ export const InAppNotificationDropdown = (): JSX.Element => {
 
 
   return (
   return (
     <Dropdown className="notification-wrapper grw-notification-dropdown" isOpen={isOpen} toggle={toggleDropdownHandler} direction="end">
     <Dropdown className="notification-wrapper grw-notification-dropdown" isOpen={isOpen} toggle={toggleDropdownHandler} direction="end">
-      <DropdownToggle className="px-3 nav-link border-0 bg-transparent" innerRef={buttonRef}>
+      <DropdownToggle className="px-3" color="primary" innerRef={buttonRef}>
         <i className="icon-bell" /> {badge}
         <i className="icon-bell" /> {badge}
       </DropdownToggle>
       </DropdownToggle>
       <DropdownMenu end>
       <DropdownMenu end>

+ 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

+ 3 - 3
apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -52,7 +52,7 @@ const AuthorInfoSkeleton = () => <Skeleton additionalClass={`${AuthorInfoStyles[
 
 
 
 
 const PageEditorModeManager = dynamic(
 const PageEditorModeManager = dynamic(
-  () => import('./PageEditorModeManager'),
+  () => import('./PageEditorModeManager').then(mod => mod.PageEditorModeManager),
   { ssr: false, loading: () => <Skeleton additionalClass={`${PageEditorModeManagerStyles['grw-page-editor-mode-manager-skeleton']}`} /> },
   { ssr: false, loading: () => <Skeleton additionalClass={`${PageEditorModeManagerStyles['grw-page-editor-mode-manager-skeleton']}`} /> },
 );
 );
 // TODO: If enable skeleton, we get hydration error when create a page from PageCreateModal
 // TODO: If enable skeleton, we get hydration error when create a page from PageCreateModal
@@ -395,9 +395,9 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
             )}
             )}
             {isAbleToChangeEditorMode && (
             {isAbleToChangeEditorMode && (
               <PageEditorModeManager
               <PageEditorModeManager
-                onPageEditorModeButtonClicked={viewType => mutateEditorMode(viewType)}
-                isBtnDisabled={!!isGuestUser || !!isReadOnlyUser}
                 editorMode={editorMode}
                 editorMode={editorMode}
+                isBtnDisabled={!!isGuestUser || !!isReadOnlyUser}
+                onPageEditorModeButtonClicked={viewType => mutateEditorMode(viewType)}
               />
               />
             )}
             )}
           </div>
           </div>

+ 0 - 127
apps/app/src/components/Navbar/PageEditorModeManager.jsx

@@ -1,127 +0,0 @@
-import React, { useCallback } from 'react';
-
-import { useTranslation } from 'next-i18next';
-import PropTypes from 'prop-types';
-import { UncontrolledTooltip } from 'reactstrap';
-
-import { useIsAdmin, useHackmdUri } from '~/stores/context';
-import { EditorMode, useIsDeviceSmallerThanMd } from '~/stores/ui';
-
-import styles from './PageEditorModeManager.module.scss';
-
-/* eslint-disable react/prop-types */
-const PageEditorModeButtonWrapper = React.memo(({
-  editorMode, isBtnDisabled, onClick, targetMode, icon, label, id,
-}) => {
-  const classNames = [`btn btn-outline-primary ${targetMode}-button px-1`];
-  if (editorMode === targetMode) {
-    classNames.push('active');
-  }
-  if (isBtnDisabled) {
-    classNames.push('disabled');
-  }
-
-  return (
-    <button
-      type="button"
-      className={classNames.join(' ')}
-      onClick={() => { onClick(targetMode) }}
-      id={id}
-      data-testid={`${targetMode}-button`}
-    >
-      <span className="d-flex flex-column flex-md-row justify-content-center">
-        <span className="grw-page-editor-mode-manager-icon me-md-1">{icon}</span>
-        <span className="grw-page-editor-mode-manager-label">{label}</span>
-      </span>
-    </button>
-  );
-});
-/* eslint-enable react/prop-types */
-
-PageEditorModeButtonWrapper.displayName = 'PageEditorModeButtonWrapper';
-
-function PageEditorModeManager(props) {
-  const {
-    editorMode, onPageEditorModeButtonClicked, isBtnDisabled,
-  } = props;
-
-  const { t } = useTranslation();
-  const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
-  const { data: hackmdUri } = useHackmdUri();
-
-  const { data: isAdmin } = useIsAdmin();
-  const isHackmdEnabled = hackmdUri != null;
-  const showHackmdBtn = isHackmdEnabled || isAdmin;
-
-  const pageEditorModeButtonClickedHandler = useCallback((viewType) => {
-    if (isBtnDisabled) {
-      return;
-    }
-    if (onPageEditorModeButtonClicked != null) {
-      onPageEditorModeButtonClicked(viewType);
-    }
-  }, [isBtnDisabled, onPageEditorModeButtonClicked]);
-
-  return (
-    <>
-      <div
-        className={`btn-group grw-page-editor-mode-manager ${styles['grw-page-editor-mode-manager']}`}
-        role="group"
-        aria-label="page-editor-mode-manager"
-        id="grw-page-editor-mode-manager"
-      >
-        {(!isDeviceSmallerThanMd || editorMode !== EditorMode.View) && (
-          <PageEditorModeButtonWrapper
-            editorMode={editorMode}
-            isBtnDisabled={isBtnDisabled}
-            onClick={pageEditorModeButtonClickedHandler}
-            targetMode={EditorMode.View}
-            icon={<i className="icon-control-play" />}
-            label={t('view')}
-          />
-        )}
-        {(!isDeviceSmallerThanMd || editorMode === EditorMode.View) && (
-          <PageEditorModeButtonWrapper
-            editorMode={editorMode}
-            isBtnDisabled={isBtnDisabled}
-            onClick={pageEditorModeButtonClickedHandler}
-            targetMode={EditorMode.Editor}
-            icon={<i className="icon-note" />}
-            label={t('Edit')}
-          />
-        )}
-        {(!isDeviceSmallerThanMd || editorMode === EditorMode.View) && showHackmdBtn && (
-          <>
-            <PageEditorModeButtonWrapper
-              editorMode={editorMode}
-              isBtnDisabled={isBtnDisabled || !isHackmdEnabled}
-              onClick={isHackmdEnabled ? pageEditorModeButtonClickedHandler : undefined}
-              targetMode={EditorMode.HackMD}
-              icon={<i className="fa fa-file-text-o" />}
-              label={t('hackmd.hack_md')}
-              id="grw-page-editor-mode-manager-hackmd-button"
-            />
-            { !isHackmdEnabled && (
-              <UncontrolledTooltip placement="top" target="grw-page-editor-mode-manager-hackmd-button" fade={false}>
-                {t('hackmd.not_set_up')}
-              </UncontrolledTooltip>
-            )}
-          </>
-        )}
-      </div>
-    </>
-  );
-
-}
-
-PageEditorModeManager.propTypes = {
-  onPageEditorModeButtonClicked: PropTypes.func,
-  isBtnDisabled: PropTypes.bool,
-  editorMode: PropTypes.string,
-};
-
-PageEditorModeManager.defaultProps = {
-  isBtnDisabled: false,
-};
-
-export default PageEditorModeManager;

+ 50 - 24
apps/app/src/components/Navbar/PageEditorModeManager.module.scss

@@ -2,41 +2,67 @@
 @use '@growi/core/scss/bootstrap/init' as bs;
 @use '@growi/core/scss/bootstrap/init' as bs;
 @use '~/styles/mixins';
 @use '~/styles/mixins';
 
 
-$btn-line-height: 1.2rem;
-
 .grw-page-editor-mode-manager :global {
 .grw-page-editor-mode-manager :global {
   .btn {
   .btn {
+    --bs-btn-font-size: 13px;
+    --bs-btn-border-width: 2px;
+
     width: 70px;
     width: 70px;
+
     white-space: nowrap;
     white-space: nowrap;
 
 
     @include mixins.border-vertical('before', 70%, 1, true);
     @include mixins.border-vertical('before', 70%, 1, true);
-
-    &.view-button,
-    &.edit-button {
-      line-height: $btn-line-height;
-      .grw-page-editor-mode-manager-icon {
-        @include bs.media-breakpoint-down(sm) {
-          font-size: 1.2rem;
-        }
-      }
-    }
-    &.hackmd-button {
-      line-height: $btn-line-height;
-      .grw-page-editor-mode-manager-icon {
-        @include bs.media-breakpoint-down(sm) {
-          font-size: 1.2rem;
-        }
-      }
-      .grw-page-editor-mode-manager-label {
-        font-size: 12px;
-        letter-spacing: -0.6px;
+    .grw-page-editor-mode-manager-icon {
+      @include bs.media-breakpoint-down(sm) {
+        font-size: 16px;
       }
       }
     }
     }
   }
   }
 }
 }
 
 
 .grw-page-editor-mode-manager-skeleton :global {
 .grw-page-editor-mode-manager-skeleton :global {
+  width: 139px;
+  height: calc(var(--bs-btn-line-height) + bs.$btn-padding-y*2 + bs.$btn-border-width*2);
+}
 
 
-  width: 213px;
-  height: calc($btn-line-height + bs.$btn-padding-y*2 + bs.$btn-border-width*2);
+// == Colors
+@include bs.color-mode(light) {
+  .grw-page-editor-mode-manager :global {
+    .btn-outline-primary {
+      $color: var(--grw-page-editor-mode-manager-btn-color, var(--grw-primary-700));
+      $bg: var(--grw-page-editor-mode-manager-btn-bg, var(--grw-primary-100));
+      $bg-rgb: var(--grw-page-editor-mode-manager-btn-bg-rgb, var(--grw-primary-100-rgb));
+
+      --bs-btn-color: #{$color};
+      --bs-btn-border-color: #{$bg};
+      --bs-btn-hover-color: #{$color};
+      --bs-btn-hover-bg: rgba(#{$bg-rgb}, 0.5);
+      --bs-btn-hover-border-color: #{$bg};
+      --bs-btn-active-color: #{$color};
+      --bs-btn-active-bg: #{$bg};
+      --bs-btn-active-border-color: #{$bg};
+      --bs-btn-disabled-color: $color;
+      --bs-btn-disabled-border-color: #{$bg};
+    }
+  }
+}
+@include bs.color-mode(dark) {
+  .grw-page-editor-mode-manager :global {
+    .btn-outline-primary {
+      $color: var(--grw-page-editor-mode-manager-btn-color, var(--grw-primary-300));
+      $bg: var(--grw-page-editor-mode-manager-btn-bg, var(--grw-primary-800));
+      $bg-rgb: var(--grw-page-editor-mode-manager-btn-bg-rgb, var(--grw-primary-800-rgb));
+
+      --bs-btn-color: #{$color};
+      --bs-btn-border-color: #{$bg};
+      --bs-btn-hover-color: #{$color};
+      --bs-btn-hover-bg: rgba(#{$bg-rgb}, 0.5);
+      --bs-btn-hover-border-color: #{$bg};
+      --bs-btn-active-color: #{$color};
+      --bs-btn-active-bg: #{$bg};
+      --bs-btn-active-border-color: #{$bg};
+      --bs-btn-disabled-color: $color;
+      --bs-btn-disabled-border-color: #{$bg};
+    }
+  }
 }
 }

+ 103 - 0
apps/app/src/components/Navbar/PageEditorModeManager.tsx

@@ -0,0 +1,103 @@
+import React, { type ReactNode, useCallback } from 'react';
+
+import { useTranslation } from 'next-i18next';
+
+import { EditorMode, useIsDeviceSmallerThanMd } from '~/stores/ui';
+
+import styles from './PageEditorModeManager.module.scss';
+
+
+type PageEditorModeButtonProps = {
+  currentEditorMode: EditorMode,
+  editorMode: EditorMode,
+  icon: ReactNode,
+  label: ReactNode,
+  isBtnDisabled?: boolean,
+  onClick?: (mode: EditorMode) => void,
+}
+const PageEditorModeButton = React.memo((props: PageEditorModeButtonProps) => {
+  const {
+    currentEditorMode, isBtnDisabled, editorMode, icon, label, onClick,
+  } = props;
+
+  const classNames = ['btn btn-outline-primary px-1'];
+  if (currentEditorMode === editorMode) {
+    classNames.push('active');
+  }
+  if (isBtnDisabled) {
+    classNames.push('disabled');
+  }
+
+  return (
+    <button
+      type="button"
+      className={classNames.join(' ')}
+      onClick={() => onClick?.(editorMode)}
+      data-testid={`${editorMode}-button`}
+    >
+      <span className="d-flex flex-column flex-md-row justify-content-center">
+        <span className="grw-page-editor-mode-manager-icon me-md-1">{icon}</span>
+        <span className="grw-page-editor-mode-manager-label">{label}</span>
+      </span>
+    </button>
+  );
+});
+
+type Props = {
+  editorMode: EditorMode | undefined,
+  onPageEditorModeButtonClicked?: (editorMode: EditorMode) => void,
+  isBtnDisabled?: boolean,
+}
+
+export const PageEditorModeManager = (props: Props): JSX.Element => {
+  const {
+    editorMode = EditorMode.View,
+    isBtnDisabled,
+    onPageEditorModeButtonClicked,
+  } = props;
+
+  const { t } = useTranslation();
+  const { data: isDeviceSmallerThanMd } = useIsDeviceSmallerThanMd();
+
+  const pageEditorModeButtonClickedHandler = useCallback((viewType) => {
+    if (isBtnDisabled ?? false) {
+      return;
+    }
+    if (onPageEditorModeButtonClicked != null) {
+      onPageEditorModeButtonClicked(viewType);
+    }
+  }, [isBtnDisabled, onPageEditorModeButtonClicked]);
+
+  return (
+    <>
+      <div
+        className={`btn-group grw-page-editor-mode-manager ${styles['grw-page-editor-mode-manager']}`}
+        role="group"
+        aria-label="page-editor-mode-manager"
+        id="grw-page-editor-mode-manager"
+      >
+        {(!isDeviceSmallerThanMd || editorMode !== EditorMode.View) && (
+          <PageEditorModeButton
+            currentEditorMode={editorMode}
+            editorMode={EditorMode.View}
+            isBtnDisabled={isBtnDisabled}
+            onClick={pageEditorModeButtonClickedHandler}
+            icon={<i className="icon-control-play" />}
+            label={t('view')}
+          />
+        )}
+        {(!isDeviceSmallerThanMd || editorMode === EditorMode.View) && (
+          <PageEditorModeButton
+            currentEditorMode={editorMode}
+            editorMode={EditorMode.Editor}
+            isBtnDisabled={isBtnDisabled}
+            onClick={pageEditorModeButtonClickedHandler}
+            icon={<i className="icon-note" />}
+            label={t('Edit')}
+          />
+        )}
+      </div>
+    </>
+  );
+
+};

+ 0 - 2
apps/app/src/components/Page/DisplaySwitcher.tsx

@@ -3,7 +3,6 @@ import React from 'react';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 
 
 
 
-import { useHackmdDraftUpdatedEffect } from '~/client/services/side-effects/hackmd-draft-updated';
 import { useHashChangedEffect } from '~/client/services/side-effects/hash-changed';
 import { useHashChangedEffect } from '~/client/services/side-effects/hash-changed';
 import { usePageUpdatedEffect } from '~/client/services/side-effects/page-updated';
 import { usePageUpdatedEffect } from '~/client/services/side-effects/page-updated';
 import { useIsEditable } from '~/stores/context';
 import { useIsEditable } from '~/stores/context';
@@ -28,7 +27,6 @@ export const DisplaySwitcher = (props: Props): JSX.Element => {
 
 
   usePageUpdatedEffect();
   usePageUpdatedEffect();
   useHashChangedEffect();
   useHashChangedEffect();
-  useHackmdDraftUpdatedEffect();
 
 
   const isViewMode = editorMode === EditorMode.View;
   const isViewMode = editorMode === EditorMode.View;
 
 

+ 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 bg-primary me-2">
+          <a
+            key={tag}
+            type="button"
+            className="grw-tag-label badge bg-primary me-2"
+            onClick={() => pushState(`tag:${tag}`)}
+          >
             {tag}
             {tag}
           </a>
           </a>
         );
         );

+ 3 - 4
apps/app/src/components/PageEditor/EditorNavbarBottom.tsx

@@ -9,7 +9,7 @@ import { useIsSlackConfigured } from '~/stores/context';
 import { useSWRxSlackChannels, useIsSlackEnabled } from '~/stores/editor';
 import { useSWRxSlackChannels, useIsSlackEnabled } from '~/stores/editor';
 import { useCurrentPagePath } from '~/stores/page';
 import { useCurrentPagePath } from '~/stores/page';
 import {
 import {
-  EditorMode, useDrawerOpened, useEditorMode, useIsDeviceSmallerThanMd,
+  useDrawerOpened, useEditorMode, useIsDeviceSmallerThanMd,
 } from '~/stores/ui';
 } from '~/stores/ui';
 
 
 
 
@@ -76,8 +76,7 @@ const EditorNavbarBottom = (): JSX.Element => {
     </div>
     </div>
   );
   );
 
 
-  const isOptionsSelectorEnabled = editorMode !== EditorMode.HackMD;
-  const isCollapsedOptionsSelectorEnabled = isOptionsSelectorEnabled && isDeviceSmallerThanMd;
+  const isCollapsedOptionsSelectorEnabled = isDeviceSmallerThanMd;
 
 
   return (
   return (
     <div className={`${isCollapsedOptionsSelectorEnabled ? 'fixed-bottom' : ''} `}>
     <div className={`${isCollapsedOptionsSelectorEnabled ? 'fixed-bottom' : ''} `}>
@@ -103,7 +102,7 @@ const EditorNavbarBottom = (): JSX.Element => {
       <div className={`flex-expand-horiz align-items-center border-top px-2 px-md-3 ${additionalClasses.join(' ')}`}>
       <div className={`flex-expand-horiz align-items-center border-top px-2 px-md-3 ${additionalClasses.join(' ')}`}>
         <form>
         <form>
           { isDeviceSmallerThanMd && renderDrawerButton() }
           { isDeviceSmallerThanMd && renderDrawerButton() }
-          { isOptionsSelectorEnabled && !isDeviceSmallerThanMd && <OptionsSelector /> }
+          { !isDeviceSmallerThanMd && <OptionsSelector /> }
         </form>
         </form>
         <form className="flex-nowrap ms-auto">
         <form className="flex-nowrap ms-auto">
           {/* Responsive Design for the SlackNotification */}
           {/* Responsive Design for the SlackNotification */}

+ 4 - 1
apps/app/src/components/PageEditor/PageEditor.tsx

@@ -19,6 +19,7 @@ import { toastError, toastSuccess } from '~/client/util/toastr';
 import { OptionsToSave } from '~/interfaces/page-operation';
 import { OptionsToSave } from '~/interfaces/page-operation';
 import { SocketEventName } from '~/interfaces/websocket';
 import { SocketEventName } from '~/interfaces/websocket';
 import {
 import {
+  useDefaultIndentSize,
   useCurrentPathname, useIsEnabledAttachTitleHeader,
   useCurrentPathname, useIsEnabledAttachTitleHeader,
   useIsEditable, useIsUploadableFile, useIsUploadableImage, useIsIndentSizeForced,
   useIsEditable, useIsUploadableFile, useIsUploadableImage, useIsIndentSizeForced,
 } from '~/stores/context';
 } from '~/stores/context';
@@ -103,6 +104,7 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
   const { data: isSlackEnabled } = useIsSlackEnabled();
   const { data: isSlackEnabled } = useIsSlackEnabled();
   const { data: isIndentSizeForced } = useIsIndentSizeForced();
   const { data: isIndentSizeForced } = useIsIndentSizeForced();
   const { data: currentIndentSize, mutate: mutateCurrentIndentSize } = useCurrentIndentSize();
   const { data: currentIndentSize, mutate: mutateCurrentIndentSize } = useCurrentIndentSize();
+  const { data: defaultIndentSize } = useDefaultIndentSize();
   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();
@@ -338,7 +340,6 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
       // Not using 'mutateGrant' to inherit the grant of the parent page
       // Not using 'mutateGrant' to inherit the grant of the parent page
       if (res.pageCreated) {
       if (res.pageCreated) {
         logger.info('Page is created', res.page._id);
         logger.info('Page is created', res.page._id);
-        globalEmitter.emit('resetInitializedHackMdStatus');
         mutateIsLatestRevision(true);
         mutateIsLatestRevision(true);
         setCreatedPageRevisionIdWithAttachment(res.page.revision);
         setCreatedPageRevisionIdWithAttachment(res.page.revision);
         await mutateCurrentPageId(res.page._id);
         await mutateCurrentPageId(res.page._id);
@@ -518,6 +519,7 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
   useEffect(() => {
   useEffect(() => {
     // do nothing if the indent size fixed
     // do nothing if the indent size fixed
     if (isIndentSizeForced == null || isIndentSizeForced) {
     if (isIndentSizeForced == null || isIndentSizeForced) {
+      mutateCurrentIndentSize(undefined);
       return;
       return;
     }
     }
 
 
@@ -572,6 +574,7 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
         <CodeMirrorEditorMain
         <CodeMirrorEditorMain
           onChange={markdownChangedHandler}
           onChange={markdownChangedHandler}
           onSave={saveWithShortcut}
           onSave={saveWithShortcut}
+          indentSize={currentIndentSize ?? defaultIndentSize}
         />
         />
       </div>
       </div>
       <div className="page-editor-preview-container flex-expand-vert d-none d-lg-flex">
       <div className="page-editor-preview-container flex-expand-vert d-none d-lg-flex">

+ 0 - 1
apps/app/src/components/PagePresentationModal.module.scss

@@ -21,7 +21,6 @@
     width: 3rem;
     width: 3rem;
     height: 3rem;
     height: 3rem;
     font-size: 1.5rem;
     font-size: 1.5rem;
-    color: var(--color-global);
     opacity: 0.3;
     opacity: 0.3;
   }
   }
 
 

+ 19 - 61
apps/app/src/components/PageStatusAlert.tsx

@@ -5,13 +5,9 @@ import * as ReactDOMServer from 'react-dom/server';
 
 
 import { useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
 import { useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
 import { useEditingMarkdown, useIsConflict } from '~/stores/editor';
 import { useEditingMarkdown, useIsConflict } from '~/stores/editor';
-import {
-  useHasDraftOnHackmd, useIsHackmdDraftUpdatingInRealtime, useRevisionIdHackmdSynced,
-} from '~/stores/hackmd';
 import { useConflictDiffModal } from '~/stores/modal';
 import { useConflictDiffModal } from '~/stores/modal';
 import { useSWRMUTxCurrentPage, useSWRxCurrentPage } from '~/stores/page';
 import { useSWRMUTxCurrentPage, useSWRxCurrentPage } from '~/stores/page';
 import { useRemoteRevisionId, useRemoteRevisionLastUpdateUser } from '~/stores/remote-latest-page';
 import { useRemoteRevisionId, useRemoteRevisionLastUpdateUser } from '~/stores/remote-latest-page';
-import { EditorMode, useEditorMode } from '~/stores/ui';
 
 
 import { Username } from './User/Username';
 import { Username } from './User/Username';
 
 
@@ -26,17 +22,13 @@ type AlertComponentContents = {
 export const PageStatusAlert = (): JSX.Element => {
 export const PageStatusAlert = (): JSX.Element => {
 
 
   const { t } = useTranslation();
   const { t } = useTranslation();
-  const { data: isHackmdDraftUpdatingInRealtime } = useIsHackmdDraftUpdatingInRealtime();
-  const { data: hasDraftOnHackmd } = useHasDraftOnHackmd();
   const { data: isConflict } = useIsConflict();
   const { data: isConflict } = useIsConflict();
   const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
   const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
   const { open: openConflictDiffModal } = useConflictDiffModal();
   const { open: openConflictDiffModal } = useConflictDiffModal();
-  const { mutate: mutateEditorMode } = useEditorMode();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
 
 
   // store remote latest page data
   // store remote latest page data
-  const { data: revisionIdHackmdSynced } = useRevisionIdHackmdSynced();
   const { data: remoteRevisionId } = useRemoteRevisionId();
   const { data: remoteRevisionId } = useRemoteRevisionId();
   const { data: remoteRevisionLastUpdateUser } = useRemoteRevisionLastUpdateUser();
   const { data: remoteRevisionLastUpdateUser } = useRemoteRevisionLastUpdateUser();
 
 
@@ -53,37 +45,23 @@ export const PageStatusAlert = (): JSX.Element => {
     openConflictDiffModal();
     openConflictDiffModal();
   }, [openConflictDiffModal]);
   }, [openConflictDiffModal]);
 
 
-  const getContentsForSomeoneEditingAlert = useCallback((): AlertComponentContents => {
-    return {
-      additionalClasses: ['bg-success', 'd-hackmd-none'],
-      label:
-  <>
-    <i className="icon-fw icon-people"></i>
-    {t('hackmd.someone_editing')}
-  </>,
-      btn:
-  <a href="#hackmd" key="btnOpenHackmdSomeoneEditing" className="btn btn-outline-white">
-    <i className="fa fa-fw fa-file-text-o me-1"></i>
-    Open HackMD Editor
-  </a>,
-    };
-  }, [t]);
-
-  const getContentsForDraftExistsAlert = useCallback((): AlertComponentContents => {
-    return {
-      additionalClasses: ['bg-success', 'd-hackmd-none'],
-      label:
-  <>
-    <i className="icon-fw icon-pencil"></i>
-    {t('hackmd.this_page_has_draft')}
-  </>,
-      btn:
-  <button type="button" onClick={() => mutateEditorMode(EditorMode.HackMD)} className="btn btn-outline-white">
-    <i className="fa fa-fw fa-file-text-o me-1"></i>
-    Open HackMD Editor
-  </button>,
-    };
-  }, [mutateEditorMode, t]);
+  // TODO: re-impl for builtin editor
+  //
+  // const getContentsForSomeoneEditingAlert = useCallback((): AlertComponentContents => {
+  //   return {
+  //     additionalClasses: ['bg-success', 'd-hackmd-none'],
+  //     label:
+  // <>
+  //   <i className="icon-fw icon-people"></i>
+  //   {t('hackmd.someone_editing')}
+  // </>,
+  //     btn:
+  // <a href="#hackmd" key="btnOpenHackmdSomeoneEditing" className="btn btn-outline-white">
+  //   <i className="fa fa-fw fa-file-text-o me-1"></i>
+  //   Open HackMD Editor
+  // </a>,
+  //   };
+  // }, [t]);
 
 
   const getContentsForUpdatedAlert = useCallback((): AlertComponentContents => {
   const getContentsForUpdatedAlert = useCallback((): AlertComponentContents => {
 
 
@@ -123,37 +101,17 @@ export const PageStatusAlert = (): JSX.Element => {
 
 
   const alertComponentContents = useMemo(() => {
   const alertComponentContents = useMemo(() => {
     const isRevisionOutdated = revision?._id !== remoteRevisionId;
     const isRevisionOutdated = revision?._id !== remoteRevisionId;
-    const isHackmdDocumentOutdated = revisionIdHackmdSynced !== remoteRevisionId;
 
 
     // 'revision?._id' and 'remoteRevisionId' are can not be undefined
     // 'revision?._id' and 'remoteRevisionId' are can not be undefined
     if (revision?._id == null || remoteRevisionId == null) { return }
     if (revision?._id == null || remoteRevisionId == null) { return }
 
 
     // when remote revision is newer than both
     // when remote revision is newer than both
-    if (isHackmdDocumentOutdated && isRevisionOutdated) {
+    if (isRevisionOutdated) {
       return getContentsForUpdatedAlert();
       return getContentsForUpdatedAlert();
     }
     }
 
 
-    // when someone editing with HackMD
-    if (isHackmdDraftUpdatingInRealtime) {
-      return getContentsForSomeoneEditingAlert();
-    }
-
-    // when the draft of HackMD is newest
-    if (hasDraftOnHackmd) {
-      return getContentsForDraftExistsAlert();
-    }
-
     return null;
     return null;
-  }, [
-    revision?._id,
-    remoteRevisionId,
-    revisionIdHackmdSynced,
-    isHackmdDraftUpdatingInRealtime,
-    hasDraftOnHackmd,
-    getContentsForUpdatedAlert,
-    getContentsForSomeoneEditingAlert,
-    getContentsForDraftExistsAlert,
-  ]);
+  }, [revision?._id, remoteRevisionId, getContentsForUpdatedAlert]);
 
 
   if (!!isGuestUser || !!isReadOnlyUser || alertComponentContents == null) { return <></> }
   if (!!isGuestUser || !!isReadOnlyUser || alertComponentContents == null) { return <></> }
 
 

+ 3 - 1
apps/app/src/components/ReactMarkdownComponents/LightBox.tsx

@@ -4,9 +4,11 @@ import FsLightbox from 'fslightbox-react';
 
 
 export const LightBox = (props) => {
 export const LightBox = (props) => {
   const [toggler, setToggler] = useState(false);
   const [toggler, setToggler] = useState(false);
+  const { node, ...rest } = props;
+
   return (
   return (
     <>
     <>
-      <img {...props.node.properties} onClick={() => setToggler(!toggler)} />
+      <img {...rest} onClick={() => setToggler(!toggler)} />
       <FsLightbox
       <FsLightbox
         toggler={toggler}
         toggler={toggler}
         sources={[props.src]}
         sources={[props.src]}

+ 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="shadow-sm">
     <div className="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
                   <label
                     className="form-label form-check-label mb-0 d-flex align-items-center text-secondary with-no-font-weight"
                     className="form-label form-check-label mb-0 d-flex align-items-center text-secondary with-no-font-weight"
@@ -145,7 +157,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="form-label form-check-label d-flex align-items-center text-secondary with-no-font-weight"
                     className="form-label form-check-label d-flex align-items-center text-secondary with-no-font-weight"

+ 3 - 2
apps/app/src/components/Sidebar/Custom/CustomSidebar.tsx

@@ -1,5 +1,6 @@
 import { Suspense } from 'react';
 import { Suspense } from 'react';
 
 
+import dynamic from 'next/dynamic';
 import Link from 'next/link';
 import Link from 'next/link';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
@@ -8,8 +9,8 @@ import { useSWRxPageByPath } from '~/stores/page';
 import { SidebarHeaderReloadButton } from '../SidebarHeaderReloadButton';
 import { SidebarHeaderReloadButton } from '../SidebarHeaderReloadButton';
 import DefaultContentSkeleton from '../Skeleton/DefaultContentSkeleton';
 import DefaultContentSkeleton from '../Skeleton/DefaultContentSkeleton';
 
 
-import { CustomSidebarSubstance } from './CustomSidebarSubstance';
 
 
+const CustomSidebarContent = dynamic(() => import('./CustomSidebarSubstance').then(mod => mod.CustomSidebarSubstance), { ssr: false });
 
 
 export const CustomSidebar = (): JSX.Element => {
 export const CustomSidebar = (): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
@@ -27,7 +28,7 @@ export const CustomSidebar = (): JSX.Element => {
       </div>
       </div>
 
 
       <Suspense fallback={<DefaultContentSkeleton />}>
       <Suspense fallback={<DefaultContentSkeleton />}>
-        <CustomSidebarSubstance />
+        <CustomSidebarContent />
       </Suspense>
       </Suspense>
     </div>
     </div>
   );
   );

+ 6 - 1
apps/app/src/components/Sidebar/Custom/CustomSidebarNotFound.tsx

@@ -1,10 +1,15 @@
 import Link from 'next/link';
 import Link from 'next/link';
+import { useTranslation } from 'react-i18next';
 
 
 export const SidebarNotFound = (): JSX.Element => {
 export const SidebarNotFound = (): JSX.Element => {
+  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>
+        {/* eslint-disable-next-line react/no-danger */}
+        <span dangerouslySetInnerHTML={{ __html: t('Create Sidebar Page') }}></span>
       </Link>
       </Link>
     </div>
     </div>
   );
   );

+ 1 - 1
apps/app/src/components/Sidebar/Custom/CustomSidebarSubstance.tsx

@@ -23,7 +23,7 @@ export const CustomSidebarSubstance = (): JSX.Element => {
 
 
   return (
   return (
     <div className={`py-3 grw-custom-sidebar-content ${styles['grw-custom-sidebar-content']}`}>
     <div className={`py-3 grw-custom-sidebar-content ${styles['grw-custom-sidebar-content']}`}>
-      { markdown === undefined
+      { markdown == null
         ? <SidebarNotFound />
         ? <SidebarNotFound />
         : (
         : (
           <RevisionRenderer
           <RevisionRenderer

+ 26 - 0
apps/app/src/components/Sidebar/PageTree/Item.module.scss

@@ -0,0 +1,26 @@
+@use '@growi/core/scss/bootstrap/init' as bs;
+
+// TODO: relocate following styles into PageTreeItem.mdoule.scss after refactoring
+// https://redmine.weseek.co.jp/issues/127544
+@include bs.color-mode(light) {
+  .pagetree-item :global {
+    .list-group-item-action {
+      .btn-page-item-control {
+        --bs-btn-bg: transparent;
+        --bs-btn-hover-bg: var(--grw-primary-300);
+        --bs-btn-active-bg: var(--grw-primary-400);
+      }
+    }
+  }
+}
+@include bs.color-mode(dark) {
+  .pagetree-item :global {
+    .list-group-item-action {
+      .btn-page-item-control {
+        --bs-btn-bg: transparent;
+        --bs-btn-hover-bg: var(--grw-primary-700);
+        --bs-btn-active-bg: var(--grw-primary-800);
+      }
+    }
+  }
+}

+ 7 - 3
apps/app/src/components/Sidebar/PageTree/Item.tsx

@@ -20,7 +20,6 @@ import { bookmark, unbookmark, resumeRenameOperation } from '~/client/services/p
 import { apiv3Put, apiv3Post } from '~/client/util/apiv3-client';
 import { apiv3Put, apiv3Post } from '~/client/util/apiv3-client';
 import { ValidationTarget } from '~/client/util/input-validator';
 import { ValidationTarget } from '~/client/util/input-validator';
 import { toastWarning, toastError, toastSuccess } from '~/client/util/toastr';
 import { toastWarning, toastError, toastSuccess } from '~/client/util/toastr';
-import { TriangleIcon } from '~/components/Icons/TriangleIcon';
 import { NotAvailableForGuest } from '~/components/NotAvailableForGuest';
 import { NotAvailableForGuest } from '~/components/NotAvailableForGuest';
 import { NotAvailableForReadOnlyUser } from '~/components/NotAvailableForReadOnlyUser';
 import { NotAvailableForReadOnlyUser } from '~/components/NotAvailableForReadOnlyUser';
 import { useSWRMUTxCurrentUserBookmarks } from '~/stores/bookmark';
 import { useSWRMUTxCurrentUserBookmarks } from '~/stores/bookmark';
@@ -38,6 +37,9 @@ import { PageItemControl } from '../../Common/Dropdown/PageItemControl';
 import { ItemNode } from './ItemNode';
 import { ItemNode } from './ItemNode';
 
 
 
 
+import styles from './Item.module.scss';
+
+
 const logger = loggerFactory('growi:cli:Item');
 const logger = loggerFactory('growi:cli:Item');
 
 
 
 
@@ -424,7 +426,9 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
       id={`pagetree-item-${page._id}`}
       id={`pagetree-item-${page._id}`}
       data-testid="grw-pagetree-item-container"
       data-testid="grw-pagetree-item-container"
       className={`grw-pagetree-item-container ${isOver ? 'grw-pagetree-is-over' : ''}
       className={`grw-pagetree-item-container ${isOver ? 'grw-pagetree-is-over' : ''}
-    ${shouldHide ? 'd-none' : ''}`}
+        pagetree-item ${styles['pagetree-item']}
+        ${shouldHide ? 'd-none' : ''}`
+      }
     >
     >
       <li
       <li
         ref={(c) => { drag(c); drop(c) }}
         ref={(c) => { drag(c); drop(c) }}
@@ -440,7 +444,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
               onClick={onClickLoadChildren}
               onClick={onClickLoadChildren}
             >
             >
               <div className="d-flex justify-content-center">
               <div className="d-flex justify-content-center">
-                <TriangleIcon />
+                <span className="material-icons-round">arrow_right</span>
               </div>
               </div>
             </button>
             </button>
           )}
           )}

+ 11 - 2
apps/app/src/components/Sidebar/PageTree/ItemsTree.module.scss

@@ -41,7 +41,7 @@ $grw-pagetree-item-container-height: 40px;
       }
       }
 
 
       .grw-pagetree-triangle-btn {
       .grw-pagetree-triangle-btn {
-        background-color: transparent;
+        border: 0;
         transition: all 0.2s ease-out;
         transition: all 0.2s ease-out;
         transform: rotate(0deg);
         transform: rotate(0deg);
 
 
@@ -67,7 +67,9 @@ $grw-pagetree-item-container-height: 40px;
 
 
     .grw-pagetree-item-container {
     .grw-pagetree-item-container {
       .grw-triangle-container {
       .grw-triangle-container {
-        min-width: 35px;
+        // TODO: ignore width frickering
+        // https://redmine.weseek.co.jp/issues/130828
+        // min-width: 35px;
         height: $grw-pagetree-item-container-height;
         height: $grw-pagetree-item-container-height;
       }
       }
     }
     }
@@ -151,3 +153,10 @@ $grw-pagetree-item-container-height: 40px;
     }
     }
   }
   }
 }
 }
+
+
+.grw-pagetree :global {
+  .grw-pagetree-triangle-btn {
+    --btn-color: var(--bs-tertiary-color);
+  }
+}

+ 46 - 46
apps/app/src/components/Sidebar/PersonalDropdown.tsx

@@ -49,59 +49,59 @@ export const PersonalDropdown = (): JSX.Element => {
           data-testid="personal-dropdown-button"
           data-testid="personal-dropdown-button"
           aria-expanded="false"
           aria-expanded="false"
         >
         >
-          <UserPicture user={currentUser} noLink noTooltip /><span className="ms-1 d-none d-lg-inline-block">&nbsp;{currentUser.name}</span>
+          <UserPicture user={currentUser} noLink noTooltip />
         </button>
         </button>
 
 
         {/* Menu */}
         {/* Menu */}
-        <div className="dropdown-menu" data-testid="personal-dropdown-menu">
-
-          <div className="px-4 pt-3 pb-2 text-center">
+        <ul className="dropdown-menu" data-testid="personal-dropdown-menu">
+          <li className="px-4 pt-3 pb-2">
             <UserPicture user={currentUser} size="lg" noLink noTooltip />
             <UserPicture user={currentUser} size="lg" noLink noTooltip />
-
-            <h5 className="mt-2">
-              {currentUser.name}
-            </h5>
-
-            <div className="my-2">
-              <i className="icon-user icon-fw"></i>{currentUser.username}<br />
-              <i className="icon-envelope icon-fw"></i><span className="grw-email-sm">{currentUser.email}</span>
+            <h5>{currentUser.name}</h5>
+            <div className="d-flex align-items-center">
+              <i className="icon-user icon-fw"></i>{currentUser.username}
             </div>
             </div>
-
-            <div className="btn-group mt-2" role="group">
-              <Link
-                href={pagePathUtils.userHomepagePath(currentUser)}
-                className="btn btn-sm btn-outline-secondary col"
-                data-testid="grw-personal-dropdown-menu-user-home"
-              >
-                <i className="icon-fw icon-home"></i>{t('personal_dropdown.home')}
-              </Link>
-              <Link
-                href="/me"
-                className="btn btn-sm btn-outline-secondary col"
-                data-testid="grw-personal-dropdown-menu-user-settings"
-              >
-                <i className="icon-fw icon-wrench"></i>{t('personal_dropdown.settings')}
-              </Link>
+            <div className="d-flex align-items-center">
+              <i className="icon-envelope icon-fw"></i><span className="grw-email-sm">{currentUser.email}</span>
             </div>
             </div>
-          </div>
-
-          <div className="dropdown-divider"></div>
-
-          <button
-            data-testid="grw-proactive-questionnaire-modal-toggle-btn"
-            type="button"
-            className="dropdown-item"
-            onClick={() => setQuestionnaireModalOpen(true)}
-          >
-            <i className="icon-fw icon-pencil"></i>{t('personal_dropdown.feedback')}
-          </button>
-
-          <div className="dropdown-divider"></div>
+          </li>
+
+          <li className="dropdown-divider"></li>
+
+          <li>
+            <Link
+              href={pagePathUtils.userHomepagePath(currentUser)}
+              className="dropdown-item"
+              data-testid="grw-personal-dropdown-menu-user-home"
+            >
+              <i className="icon-fw icon-home"></i>{t('personal_dropdown.home')}
+            </Link>
+          </li>
+          <li>
+            <Link
+              href="/me"
+              className="dropdown-item"
+              data-testid="grw-personal-dropdown-menu-user-settings"
+            >
+              <i className="icon-fw icon-wrench"></i>{t('personal_dropdown.settings')}
+            </Link>
+          </li>
+          <li>
+            <button
+              data-testid="grw-proactive-questionnaire-modal-toggle-btn"
+              type="button"
+              className="dropdown-item"
+              onClick={() => setQuestionnaireModalOpen(true)}
+            >
+              <i className="icon-fw icon-pencil"></i>{t('personal_dropdown.feedback')}
+            </button>
+          </li>
+          <li>
+            <button type="button" className="dropdown-item" onClick={logoutHandler}>
+              <i className="icon-fw icon-power"></i>{t('Sign out')}
+            </button>
+          </li>
+        </ul>
 
 
-          <button type="button" className="dropdown-item" onClick={logoutHandler}>
-            <i className="icon-fw icon-power"></i>{t('Sign out')}
-          </button>
-        </div>
       </div>
       </div>
 
 
       <ProactiveQuestionnaireModal isOpen={isQuestionnaireModalOpen} onClose={() => setQuestionnaireModalOpen(false)} />
       <ProactiveQuestionnaireModal isOpen={isQuestionnaireModalOpen} onClose={() => setQuestionnaireModalOpen(false)} />

+ 11 - 8
apps/app/src/components/Sidebar/RecentChanges/RecentChangesSubstance.tsx

@@ -5,8 +5,8 @@ import React, {
 import { isPopulated, type IPageHasId } from '@growi/core';
 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 Link from 'next/link';
 
 
+import { useKeywordManager } from '~/client/services/search-operation';
 import FormattedDistanceDate from '~/components/FormattedDistanceDate';
 import FormattedDistanceDate from '~/components/FormattedDistanceDate';
 import InfiniteScroll from '~/components/InfiniteScroll';
 import InfiniteScroll from '~/components/InfiniteScroll';
 import PagePathHierarchicalLink from '~/components/PagePathHierarchicalLink';
 import PagePathHierarchicalLink from '~/components/PagePathHierarchicalLink';
@@ -27,7 +27,8 @@ 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 => {
@@ -47,7 +48,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);
@@ -68,14 +69,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 bg-primary me-2 small"
         className="grw-tag-label badge bg-primary me-2 small"
-        prefetch={false}
+        onClick={() => onClickTag?.(tag.name)}
       >
       >
         {tag.name}
         {tag.name}
-      </Link>
+      </a>
     );
     );
   });
   });
 
 
@@ -156,6 +157,8 @@ export const RecentChangesContent = ({ isSmall }: ContentProps): JSX.Element =>
   const swrInifinitexRecentlyUpdated = useSWRINFxRecentlyUpdated(PER_PAGE, { suspense: true });
   const swrInifinitexRecentlyUpdated = useSWRINFxRecentlyUpdated(PER_PAGE, { suspense: true });
   const { data } = swrInifinitexRecentlyUpdated;
   const { data } = swrInifinitexRecentlyUpdated;
 
 
+  const { pushState } = useKeywordManager();
+
   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);
 
 
@@ -168,7 +171,7 @@ export const RecentChangesContent = ({ isSmall }: ContentProps): 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={isSmall} />
+              <PageItem key={page._id} page={page} isSmall={isSmall} onClickTag={tagName => pushState(`tag:${tagName}`)} />
             ))
             ))
           }
           }
         </InfiniteScroll>
         </InfiniteScroll>

+ 28 - 5
apps/app/src/components/Sidebar/Sidebar.module.scss

@@ -9,17 +9,13 @@
   // set the max value that should be taken when sticky
   // set the max value that should be taken when sticky
   height: 100vh;
   height: 100vh;
 
 
+  border-right : 1px solid var(--bs-border-color);
 
 
   .data-layout-container {
   .data-layout-container {
     display: flex;
     display: flex;
     flex-direction: row;
     flex-direction: row;
     height: 100vh;
     height: 100vh;
     margin-top: 0px;
     margin-top: 0px;
-    // css-teprsg
-    > div:nth-of-type(2) {
-      padding-left: unset !important;
-      margin-left: unset !important;
-    }
   }
   }
   .navigation {
   .navigation {
     .grw-navigation-wrap {
     .grw-navigation-wrap {
@@ -218,3 +214,30 @@
     }
     }
   }
   }
 }
 }
+
+
+.grw-sidebar :global {
+  .grw-contextual-navigation {
+    backdrop-filter: blur(20px);
+  }
+}
+@include bs.color-mode(light) {
+  .grw-sidebar :global {
+    --bs-border-color: var(--grw-highlight-200);
+
+    .grw-contextual-navigation {
+      background-color: rgba(var(--grw-highlight-100-rgb), .5);
+    }
+  }
+}
+
+@include bs.color-mode(dark) {
+  .grw-sidebar :global {
+    --bs-color: var(--bs-gray-400);
+    --bs-border-color: var(--grw-highlight-800);
+
+    .grw-contextual-navigation {
+      background-color: rgba(var(--grw-highlight-800-rgb), .5);
+    }
+  }
+}

+ 14 - 0
apps/app/src/components/Sidebar/SidebarContents.module.scss

@@ -0,0 +1,14 @@
+@use '@growi/core/scss/bootstrap/init' as bs;
+
+.grw-sidebar-contents :global {
+
+  --bs-heading-color: var(--bs-tertiary-color);
+  --bs-body-color: var(--bs-secondary-color);
+  --bs-link-color-rgb: var(--bs-secondary-color-rgb);
+  --bs-link-opacity: .75;
+
+  .list-group-item {
+    --bs-list-group-bg: transparent;
+  }
+
+}

+ 6 - 1
apps/app/src/components/Sidebar/SidebarContents.tsx

@@ -9,6 +9,9 @@ import { PageTree } from './PageTree';
 import { RecentChanges } from './RecentChanges';
 import { RecentChanges } from './RecentChanges';
 import Tag from './Tag';
 import Tag from './Tag';
 
 
+import styles from './SidebarContents.module.scss';
+
+
 export const SidebarContents = memo(() => {
 export const SidebarContents = memo(() => {
   const { data: currentSidebarContents } = useCurrentSidebarContents();
   const { data: currentSidebarContents } = useCurrentSidebarContents();
 
 
@@ -31,7 +34,9 @@ export const SidebarContents = memo(() => {
   }
   }
 
 
   return (
   return (
-    <Contents />
+    <div className={`grw-sidebar-contents ${styles['grw-sidebar-contents']}`}>
+      <Contents />
+    </div>
   );
   );
 });
 });
 SidebarContents.displayName = 'SidebarContents';
 SidebarContents.displayName = 'SidebarContents';

+ 68 - 29
apps/app/src/components/Sidebar/SidebarNav.module.scss

@@ -3,31 +3,14 @@
 @use '~/styles/variables' as var;
 @use '~/styles/variables' as var;
 
 
 .grw-sidebar-nav :global {
 .grw-sidebar-nav :global {
-  $sidebar-nav-button-height: 55px;
-
-  %fukidashi-for-active {
-    position: relative;
-
-    // speech balloon
-    &:after {
-      position: absolute;
-      right: -0.1em;
-      display: block;
-      width: 0;
-      content: '';
-      border: 9px solid transparent;
-      border-right-color: white;
-      border-left-width: 0;
-      transform: translateY(-#{$sidebar-nav-button-height / 2});
-    }
-  }
-
   // set position and z-index to prevent dropdowns covered by other element
   // set position and z-index to prevent dropdowns covered by other element
   position: relative;
   position: relative;
   z-index: bs.$zindex-fixed;
   z-index: bs.$zindex-fixed;
 
 
   height: 100vh;
   height: 100vh;
 
 
+  border-right : 1px solid var(--bs-border-color);
+
   .grw-logo {
   .grw-logo {
     svg {
     svg {
       width: var.$grw-logo-width;
       width: var.$grw-logo-width;
@@ -50,33 +33,47 @@
 
 
   .btn {
   .btn {
     width: var.$grw-sidebar-nav-width;
     width: var.$grw-sidebar-nav-width;
+    height: var.$grw-sidebar-nav-height;
+    padding-top: .75rem;
+    padding-bottom: .75rem;
     line-height: 1em;
     line-height: 1em;
+    border: 0;
     border-radius: 0;
     border-radius: 0;
     box-shadow: none !important;
     box-shadow: none !important;
 
 
     // icon opacity
     // icon opacity
     &:not(.active) {
     &:not(.active) {
       i {
       i {
-        opacity: 0.4;
+        opacity: 0.7;
       }
       }
       &:hover,
       &:hover,
       &:focus {
       &:focus {
         i {
         i {
-          opacity: 0.7;
+          opacity: 0.8;
         }
         }
       }
       }
     }
     }
   }
   }
 
 
   .grw-sidebar-nav-primary-container {
   .grw-sidebar-nav-primary-container {
-    .btn {
-      padding: 1em;
-      i {
-        font-size: 2.3em;
-      }
+    $btn-active-indicator-height: 34px;
 
 
+    .btn {
       &.active {
       &.active {
-        @extend %fukidashi-for-active;
+        position: relative;
+
+        // indicator
+        &:after {
+          position: absolute;
+          top: 0;
+          left: 0;
+          display: block;
+          width: 3px;
+          height: $btn-active-indicator-height;
+          content: '';
+          background-color: var(--bs-primary);
+          transform: translateY(#{(var.$grw-sidebar-nav-height - $btn-active-indicator-height) / 2});
+        }
       }
       }
     }
     }
   }
   }
@@ -86,10 +83,52 @@
     bottom: 1.5rem;
     bottom: 1.5rem;
 
 
     .btn {
     .btn {
-      padding: 0.9em;
       i {
       i {
-        font-size: 1.5em;
+        opacity: 0.4;
       }
       }
     }
     }
   }
   }
 }
 }
+
+
+// == Colors
+.grw-sidebar-nav :global {
+  .btn.btn-primary {
+    --bs-btn-bg: transparent;
+    --bs-btn-active-bg: transparent;
+    --bs-btn-hover-color: var(
+      --grw-sidebar-nav-btn-hover-color,
+      var(
+        --grw-sidebar-nav-btn-color,
+        var(--bs-btn-color)
+      ),
+    );
+    --bs-btn-active-color: var(
+      --grw-sidebar-nav-btn-active-color,
+      var(
+        --grw-sidebar-nav-btn-color,
+        var(--bs-btn-color)
+      ),
+    );
+  }
+}
+@include bs.color-mode(light) {
+  .grw-sidebar-nav :global {
+    background-color: var(--grw-sidebar-nav-bg, var(--grw-highlight-100));
+
+    .btn-primary {
+      --bs-btn-color: var(--grw-sidebar-nav-btn-color, var(--grw-primary-500));
+      --bs-btn-hover-bg: var(--grw-sidebar-nav-btn-hover-bg, var(--grw-highlight-300));
+    }
+  }
+}
+@include bs.color-mode(dark) {
+  .grw-sidebar-nav :global {
+    background-color: var(--grw-sidebar-nav-bg, var(--grw-highlight-800));
+
+    .btn-primary {
+      --bs-btn-color: var(--grw-sidebar-nav-btn-color, var(--grw-primary-400));
+      --bs-btn-hover-bg: var(--grw-sidebar-nav-btn-hover-bg, var(--grw-highlight-700));
+    }
+  }
+}

+ 6 - 12
apps/app/src/components/Sidebar/SidebarNav.tsx

@@ -115,26 +115,20 @@ export const SidebarNav: FC<Props> = (props: Props) => {
       </div>
       </div>
 
 
       <div className="grw-sidebar-nav-primary-container" data-vrt-blackout-sidebar-nav>
       <div className="grw-sidebar-nav-primary-container" data-vrt-blackout-sidebar-nav>
-        {/* 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} />
         <PrimaryItem contents={SidebarContentsType.CUSTOM} label="Custom Sidebar" iconName="code" onItemSelected={onItemSelected} />
         <PrimaryItem contents={SidebarContentsType.CUSTOM} label="Custom Sidebar" iconName="code" onItemSelected={onItemSelected} />
         <PrimaryItem contents={SidebarContentsType.RECENT} label="Recent Changes" iconName="update" onItemSelected={onItemSelected} />
         <PrimaryItem contents={SidebarContentsType.RECENT} label="Recent Changes" iconName="update" onItemSelected={onItemSelected} />
-        {/* <PrimaryItem id="tag" label="Tags" iconName="icon-tag" /> */}
-        {/* <PrimaryItem id="favorite" label="Favorite" iconName="fa fa-bookmark-o" /> */}
-        <PrimaryItem contents={SidebarContentsType.TAG} label="Tags" iconName="local_offer" onItemSelected={onItemSelected} />
-        {/* <PrimaryItem id="favorite" label="Favorite" iconName="icon-star" /> */}
-        {/* eslint-enable max-len */}
         <PrimaryItem contents={SidebarContentsType.BOOKMARKS} label="Bookmarks" iconName="bookmark" onItemSelected={onItemSelected} />
         <PrimaryItem contents={SidebarContentsType.BOOKMARKS} label="Bookmarks" iconName="bookmark" onItemSelected={onItemSelected} />
+        <PrimaryItem contents={SidebarContentsType.TAG} label="Tags" iconName="local_offer" onItemSelected={onItemSelected} />
+        <InAppNotificationDropdown />
       </div>
       </div>
       <div className="grw-sidebar-nav-secondary-container">
       <div className="grw-sidebar-nav-secondary-container">
-        <AppearanceModeDropdown isAuthenticated={isAuthenticated} />
+        {/* TODO: This setting will be consolidated in "Settings" on My Page, so delete it from here. */}
+        {/* <AppearanceModeDropdown isAuthenticated={isAuthenticated} /> */}
         <PersonalDropdown />
         <PersonalDropdown />
-        <InAppNotificationDropdown />
-
-        {isAdmin && <SecondaryItem label="Admin" iconName="settings" href="/admin" />}
-        {/* <SecondaryItem label="Draft" iconName="file_copy" href="/me/drafts" /> */}
         <SecondaryItem label="Help" iconName="help" href={growiCloudUri != null ? 'https://growi.cloud/help/' : 'https://docs.growi.org'} isBlank />
         <SecondaryItem label="Help" iconName="help" href={growiCloudUri != null ? 'https://growi.cloud/help/' : 'https://docs.growi.org'} isBlank />
-        <SecondaryItem label="Trash" iconName="delete" href="/trash" />
+        {isAdmin && <SecondaryItem label="Admin" iconName="settings" href="/admin" />}
+        <SecondaryItem label="Trash" href="/trash" iconName="delete" />
       </div>
       </div>
     </div>
     </div>
   );
   );

+ 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 bg-primary me-2"
         className="grw-tag-label badge bg-primary me-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="ms-4 my-auto py-1 px-2 list-tag-count badge bg-primary">{tag.count}</div>
           <div className="ms-4 my-auto py-1 px-2 list-tag-count badge bg-primary">{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-start mb-5">
+      <div className="list-group text-start mb-5">
         {generateTagList(tagData)}
         {generateTagList(tagData)}
-      </ul>
+      </div>
       {isPaginationShown
       {isPaginationShown
       && (
       && (
         <PaginationWrapper
         <PaginationWrapper

+ 0 - 1
apps/app/src/interfaces/page-operation.ts

@@ -34,5 +34,4 @@ export type OptionsToSave = {
   pageTags: string[] | null;
   pageTags: string[] | null;
   grantUserGroupId?: string | null;
   grantUserGroupId?: string | null;
   grantUserGroupName?: string | null;
   grantUserGroupName?: string | null;
-  isSyncRevisionToHackmd?: boolean;
 };
 };

+ 0 - 3
apps/app/src/interfaces/websocket.ts

@@ -22,9 +22,6 @@ export const SocketEventName = {
   PageUpdated: 'page:update',
   PageUpdated: 'page:update',
   PageDeleted: 'page:delete',
   PageDeleted: 'page:delete',
 
 
-  // Hackmd
-  EditingWithHackmd: 'page:editingWithHackmd',
-
 } as const;
 } as const;
 export type SocketEventName = typeof SocketEventName[keyof typeof SocketEventName];
 export type SocketEventName = typeof SocketEventName[keyof typeof SocketEventName];
 
 

+ 0 - 1
apps/app/src/models/admin/import-option-for-pages.js

@@ -6,7 +6,6 @@ const DEFAULT_PROPS = {
   makePublicForGrant4: false,
   makePublicForGrant4: false,
   makePublicForGrant5: false,
   makePublicForGrant5: false,
   initPageMetadatas: false,
   initPageMetadatas: false,
-  initHackmdDatas: false,
 };
 };
 
 
 class ImportOptionForPages extends GrowiArchiveImportOption {
 class ImportOptionForPages extends GrowiArchiveImportOption {

+ 2 - 10
apps/app/src/pages/[[...path]].page.tsx

@@ -33,14 +33,13 @@ import {
   useIsForbidden, useIsSharedUser,
   useIsForbidden, useIsSharedUser,
   useIsEnabledStaleNotification, useIsIdenticalPath,
   useIsEnabledStaleNotification, useIsIdenticalPath,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useDisableLinkSharing,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useDisableLinkSharing,
-  useHackmdUri, useDefaultIndentSize, useIsIndentSizeForced,
+  useDefaultIndentSize, useIsIndentSizeForced,
   useIsAclEnabled, useIsSearchPage, useIsEnabledAttachTitleHeader,
   useIsAclEnabled, useIsSearchPage, useIsEnabledAttachTitleHeader,
   useCsrfToken, useIsSearchScopeChildrenAsDefault, useIsEnabledMarp, useCurrentPathname,
   useCsrfToken, useIsSearchScopeChildrenAsDefault, useIsEnabledMarp, useCurrentPathname,
   useIsSlackConfigured, useRendererConfig, useGrowiCloudUri,
   useIsSlackConfigured, useRendererConfig, useGrowiCloudUri,
   useEditorConfig, useIsAllReplyShown, useIsUploadableFile, useIsUploadableImage, useIsContainerFluid, useIsNotCreatable,
   useEditorConfig, useIsAllReplyShown, useIsUploadableFile, useIsUploadableImage, useIsContainerFluid, useIsNotCreatable,
 } from '~/stores/context';
 } from '~/stores/context';
 import { useEditingMarkdown } from '~/stores/editor';
 import { useEditingMarkdown } from '~/stores/editor';
-import { useHasDraftOnHackmd, usePageIdOnHackmd, useRevisionIdHackmdSynced } from '~/stores/hackmd';
 import {
 import {
   useSWRxCurrentPage, useSWRMUTxCurrentPage, useSWRxIsGrantNormalized, useCurrentPageId,
   useSWRxCurrentPage, useSWRMUTxCurrentPage, useSWRxIsGrantNormalized, useCurrentPageId,
   useIsNotFound, useIsLatestRevision, useTemplateTagData, useTemplateBodyData,
   useIsNotFound, useIsLatestRevision, useTemplateTagData, useTemplateBodyData,
@@ -154,7 +153,6 @@ type Props = CommonProps & {
   isAclEnabled: boolean,
   isAclEnabled: boolean,
   // hasSlackConfig: boolean,
   // hasSlackConfig: boolean,
   drawioUri: string | null,
   drawioUri: string | null,
-  hackmdUri: string,
   noCdn: string,
   noCdn: string,
   // highlightJsStyle: string,
   // highlightJsStyle: string,
   isAllReplyShown: boolean,
   isAllReplyShown: boolean,
@@ -210,7 +208,6 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   // useIsMailerSetup(props.isMailerSetup);
   // useIsMailerSetup(props.isMailerSetup);
   useIsAclEnabled(props.isAclEnabled);
   useIsAclEnabled(props.isAclEnabled);
   // useHasSlackConfig(props.hasSlackConfig);
   // useHasSlackConfig(props.hasSlackConfig);
-  useHackmdUri(props.hackmdUri);
   // useNoCdn(props.noCdn);
   // useNoCdn(props.noCdn);
   useDefaultIndentSize(props.adminPreferredIndentSize);
   useDefaultIndentSize(props.adminPreferredIndentSize);
   useIsIndentSizeForced(props.isIndentSizeForced);
   useIsIndentSizeForced(props.isIndentSizeForced);
@@ -230,8 +227,6 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   const pagePath = pageWithMeta?.data.path ?? props.currentPathname;
   const pagePath = pageWithMeta?.data.path ?? props.currentPathname;
   const revisionBody = pageWithMeta?.data.revision?.body;
   const revisionBody = pageWithMeta?.data.revision?.body;
 
 
-  usePageIdOnHackmd(pageWithMeta?.data.pageIdOnHackmd);
-  useHasDraftOnHackmd(pageWithMeta?.data.hasDraftOnHackmd ?? false);
   useCurrentPathname(props.currentPathname);
   useCurrentPathname(props.currentPathname);
 
 
   useSWRxCurrentPage(pageWithMeta?.data ?? null); // store initial data
   useSWRxCurrentPage(pageWithMeta?.data ?? null); // store initial data
@@ -248,7 +243,6 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   const { mutate: mutateSelectedGrant } = useSelectedGrant();
   const { mutate: mutateSelectedGrant } = useSelectedGrant();
 
 
   const { mutate: mutateRemoteRevisionId } = useRemoteRevisionId();
   const { mutate: mutateRemoteRevisionId } = useRemoteRevisionId();
-  const { mutate: mutateRevisionIdHackmdSynced } = useRevisionIdHackmdSynced();
 
 
   const { mutate: mutateTemplateTagData } = useTemplateTagData();
   const { mutate: mutateTemplateTagData } = useTemplateTagData();
   const { mutate: mutateTemplateBodyData } = useTemplateBodyData();
   const { mutate: mutateTemplateBodyData } = useTemplateBodyData();
@@ -301,8 +295,7 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
 
 
   useEffect(() => {
   useEffect(() => {
     mutateRemoteRevisionId(pageWithMeta?.data.revision?._id);
     mutateRemoteRevisionId(pageWithMeta?.data.revision?._id);
-    mutateRevisionIdHackmdSynced(pageWithMeta?.data.revisionHackmdSynced);
-  }, [mutateRemoteRevisionId, mutateRevisionIdHackmdSynced, pageWithMeta?.data.revision?._id, pageWithMeta?.data.revisionHackmdSynced]);
+  }, [mutateRemoteRevisionId, pageWithMeta?.data.revision?._id]);
 
 
   useEffect(() => {
   useEffect(() => {
     mutateCurrentPageId(pageId ?? null);
     mutateCurrentPageId(pageId ?? null);
@@ -562,7 +555,6 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
   props.isAclEnabled = aclService.isAclEnabled();
   props.isAclEnabled = aclService.isAclEnabled();
   // props.hasSlackConfig = slackNotificationService.hasSlackConfig();
   // props.hasSlackConfig = slackNotificationService.hasSlackConfig();
   props.drawioUri = configManager.getConfig('crowi', 'app:drawioUri');
   props.drawioUri = configManager.getConfig('crowi', 'app:drawioUri');
-  props.hackmdUri = configManager.getConfig('crowi', 'app:hackmdUri');
   props.noCdn = configManager.getConfig('crowi', 'app:noCdn');
   props.noCdn = configManager.getConfig('crowi', 'app:noCdn');
   // props.highlightJsStyle = configManager.getConfig('crowi', 'customize:highlightJsStyle');
   // props.highlightJsStyle = configManager.getConfig('crowi', 'customize:highlightJsStyle');
   props.isAllReplyShown = configManager.getConfig('crowi', 'customize:isAllReplyShown');
   props.isAllReplyShown = configManager.getConfig('crowi', 'customize:isAllReplyShown');

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff