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

Merge branch 'dev/7.0.x' into support/129222-use-new-accordion

soumaeda 2 лет назад
Родитель
Сommit
dd63d78fdd
100 измененных файлов с 641 добавлено и 662 удалено
  1. 36 1
      CHANGELOG.md
  2. 1 2
      README.md
  3. 1 2
      README_JP.md
  4. 1 0
      _obsolete/packages/.eslintignore
  5. 0 0
      _obsolete/packages/hackmd/.eslintignore
  6. 0 0
      _obsolete/packages/hackmd/.gitignore
  7. 0 0
      _obsolete/packages/hackmd/package.json
  8. 0 0
      _obsolete/packages/hackmd/src/hackmd-agent.js
  9. 0 0
      _obsolete/packages/hackmd/src/hackmd-styles.ts
  10. 0 0
      _obsolete/packages/hackmd/src/index.ts
  11. 0 0
      _obsolete/packages/hackmd/src/style.scss
  12. 0 0
      _obsolete/packages/hackmd/tsconfig.json
  13. 0 0
      _obsolete/packages/hackmd/vite.config.js
  14. 0 2
      apps/app/.env.development
  15. 0 0
      apps/app/_obsolete/src/client/services/side-effects/hackmd-draft-updated.ts
  16. 0 0
      apps/app/_obsolete/src/components/PageEditorByHackmd.tsx
  17. 0 0
      apps/app/_obsolete/src/components/PageEditorByHackmd/HackmdEditor.jsx
  18. 0 0
      apps/app/_obsolete/src/interfaces/hackmd.ts
  19. 0 0
      apps/app/_obsolete/src/server/routes/hackmd.js
  20. 0 0
      apps/app/_obsolete/src/stores/hackmd.ts
  21. 0 0
      apps/app/_obsolete/src/styles/theme/_apply-colors-dark.scss
  22. 0 0
      apps/app/_obsolete/src/styles/theme/_apply-colors-light.scss
  23. 0 0
      apps/app/_obsolete/src/styles/theme/_hsl-functions.scss
  24. 0 0
      apps/app/_obsolete/src/styles/theme/_hsl-reboot-bootstrap-theme-colors.scss
  25. 0 0
      apps/app/_obsolete/src/styles/theme/_reboot-bootstrap-border-colors.scss
  26. 0 0
      apps/app/_obsolete/src/styles/theme/_reboot-bootstrap-buttons.scss
  27. 0 0
      apps/app/_obsolete/src/styles/theme/_reboot-bootstrap-colors.scss
  28. 0 0
      apps/app/_obsolete/src/styles/theme/_reboot-bootstrap-dropdown.scss
  29. 0 0
      apps/app/_obsolete/src/styles/theme/_reboot-bootstrap-nav.scss
  30. 0 0
      apps/app/_obsolete/src/styles/theme/_reboot-bootstrap-tables.scss
  31. 0 0
      apps/app/_obsolete/src/styles/theme/_reboot-bootstrap-text.scss
  32. 0 0
      apps/app/_obsolete/src/styles/theme/_reboot-bootstrap-theme-colors.scss
  33. 0 0
      apps/app/_obsolete/src/styles/theme/_reboot-toastr-colors.scss
  34. 0 0
      apps/app/_obsolete/src/styles/theme/apply-colors.scss
  35. 0 0
      apps/app/_obsolete/src/styles/theme/mixins/_count-badge.scss
  36. 0 0
      apps/app/_obsolete/src/styles/theme/mixins/_hsl-badge.scss
  37. 0 0
      apps/app/_obsolete/src/styles/theme/mixins/_hsl-button.scss
  38. 0 0
      apps/app/_obsolete/src/styles/theme/mixins/_list-group.scss
  39. 0 0
      apps/app/_obsolete/src/styles/theme/mixins/_page-editor-mode-manager.scss
  40. 0 2
      apps/app/package.json
  41. 5 5
      apps/app/public/static/locales/en_US/admin.json
  42. 2 22
      apps/app/public/static/locales/en_US/translation.json
  43. 7 6
      apps/app/public/static/locales/ja_JP/admin.json
  44. 2 22
      apps/app/public/static/locales/ja_JP/translation.json
  45. 5 5
      apps/app/public/static/locales/zh_CN/admin.json
  46. 2 22
      apps/app/public/static/locales/zh_CN/translation.json
  47. 21 2
      apps/app/src/client/services/AdminCustomizeContainer.js
  48. 0 19
      apps/app/src/client/services/page-operation.ts
  49. 2 4
      apps/app/src/client/services/side-effects/page-updated.ts
  50. 4 0
      apps/app/src/components/Admin/Customize/Customize.jsx
  51. 0 28
      apps/app/src/components/Admin/Customize/CustomizeFunctionSetting.tsx
  52. 70 0
      apps/app/src/components/Admin/Customize/CustomizePresentationSetting.tsx
  53. 0 13
      apps/app/src/components/Admin/ImportData/GrowiArchive/ImportCollectionConfigurationModal.jsx
  54. 1 2
      apps/app/src/components/Bookmarks/BookmarkFolderItem.tsx
  55. 4 2
      apps/app/src/components/Bookmarks/BookmarkFolderTree.module.scss
  56. 0 15
      apps/app/src/components/Icons/TriangleIcon.tsx
  57. 1 1
      apps/app/src/components/InAppNotification/InAppNotificationDropdown.tsx
  58. 3 3
      apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  59. 0 127
      apps/app/src/components/Navbar/PageEditorModeManager.jsx
  60. 50 24
      apps/app/src/components/Navbar/PageEditorModeManager.module.scss
  61. 103 0
      apps/app/src/components/Navbar/PageEditorModeManager.tsx
  62. 0 2
      apps/app/src/components/Page/DisplaySwitcher.tsx
  63. 3 4
      apps/app/src/components/PageEditor/EditorNavbarBottom.tsx
  64. 0 1
      apps/app/src/components/PageEditor/PageEditor.tsx
  65. 0 1
      apps/app/src/components/PagePresentationModal.module.scss
  66. 19 61
      apps/app/src/components/PageStatusAlert.tsx
  67. 3 1
      apps/app/src/components/ReactMarkdownComponents/LightBox.tsx
  68. 3 2
      apps/app/src/components/Sidebar/Custom/CustomSidebar.tsx
  69. 6 1
      apps/app/src/components/Sidebar/Custom/CustomSidebarNotFound.tsx
  70. 1 1
      apps/app/src/components/Sidebar/Custom/CustomSidebarSubstance.tsx
  71. 26 0
      apps/app/src/components/Sidebar/PageTree/Item.module.scss
  72. 7 3
      apps/app/src/components/Sidebar/PageTree/Item.tsx
  73. 11 2
      apps/app/src/components/Sidebar/PageTree/ItemsTree.module.scss
  74. 46 46
      apps/app/src/components/Sidebar/PersonalDropdown.tsx
  75. 28 5
      apps/app/src/components/Sidebar/Sidebar.module.scss
  76. 14 0
      apps/app/src/components/Sidebar/SidebarContents.module.scss
  77. 6 1
      apps/app/src/components/Sidebar/SidebarContents.tsx
  78. 68 29
      apps/app/src/components/Sidebar/SidebarNav.module.scss
  79. 6 12
      apps/app/src/components/Sidebar/SidebarNav.tsx
  80. 0 1
      apps/app/src/interfaces/page-operation.ts
  81. 0 3
      apps/app/src/interfaces/websocket.ts
  82. 0 1
      apps/app/src/models/admin/import-option-for-pages.js
  83. 2 10
      apps/app/src/pages/[[...path]].page.tsx
  84. 0 1
      apps/app/src/pages/_app.page.tsx
  85. 8 1
      apps/app/src/server/.node-dev.json
  86. 0 1
      apps/app/src/server/models/interfaces/page-operation.ts
  87. 0 43
      apps/app/src/server/models/obsolete-page.js
  88. 0 1
      apps/app/src/server/models/page-operation.ts
  89. 0 3
      apps/app/src/server/models/page.ts
  90. 1 2
      apps/app/src/server/models/serializers/page-serializer.js
  91. 1 3
      apps/app/src/server/models/vo/s2c-message.js
  92. 24 4
      apps/app/src/server/routes/apiv3/customize-setting.js
  93. 0 6
      apps/app/src/server/routes/apiv3/overwrite-params/pages.js
  94. 0 7
      apps/app/src/server/routes/index.js
  95. 1 2
      apps/app/src/server/routes/page.js
  96. 0 12
      apps/app/src/server/service/config-loader.ts
  97. 7 18
      apps/app/src/server/service/page.ts
  98. 3 15
      apps/app/src/server/service/system-events/sync-page-status.ts
  99. 0 4
      apps/app/src/stores/context.tsx
  100. 26 21
      apps/app/src/stores/page.tsx

+ 36 - 1
CHANGELOG.md

@@ -1,6 +1,6 @@
 # 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.*
 
 
@@ -23,6 +23,41 @@
 - fix: Do not work img tag if use style property (#7988) @jam411
 - fix: Do not work img tag if use style property (#7988) @jam411
 - fix: "Searching..." label appearing unnecessarily (#7990) @yuki-takei
 - 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

+ 1 - 2
README.md

@@ -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

+ 1 - 2
README_JP.md

@@ -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 と連携することが可能です。

+ 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",
@@ -245,7 +244,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": "只有那些知道链接的人",

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

+ 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) {

+ 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 */

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

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

+ 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 */}

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

@@ -340,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);

+ 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]}

+ 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)} />

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

+ 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');

+ 0 - 1
apps/app/src/pages/_app.page.tsx

@@ -21,7 +21,6 @@ import { registerTransformerForObjectId } from './utils/objectid-transformer';
 import '~/styles/prebuilt/vendor.css';
 import '~/styles/prebuilt/vendor.css';
 import '~/styles/font-icons.scss';
 import '~/styles/font-icons.scss';
 import '~/styles/style-app.scss';
 import '~/styles/style-app.scss';
-import '~/styles/prebuilt/apply-colors.css';
 
 
 
 
 const isDev = process.env.NODE_ENV === 'development';
 const isDev = process.env.NODE_ENV === 'development';

+ 8 - 1
apps/app/src/server/.node-dev.json

@@ -1,3 +1,10 @@
 {
 {
-  "ignore": ["package.json", ".next", "public/static"]
+  "ignore": [
+    "package.json",
+    ".next",
+    "public/static",
+
+    "// ignore watching preset theme updates",
+    "packages/preset-themes/dist/themes/manifest.json"
+  ]
 }
 }

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

@@ -24,7 +24,6 @@ export type IUserForResuming = {
 export type IOptionsForUpdate = {
 export type IOptionsForUpdate = {
   grant?: PageGrant,
   grant?: PageGrant,
   grantUserGroupId?: ObjectIdLike,
   grantUserGroupId?: ObjectIdLike,
-  isSyncRevisionToHackmd?: boolean,
   overwriteScopesOfDescendants?: boolean,
   overwriteScopesOfDescendants?: boolean,
 };
 };
 
 

+ 0 - 43
apps/app/src/server/models/obsolete-page.js

@@ -701,49 +701,6 @@ export const getPageSchema = (crowi) => {
     await this.updateMany({ _id: { $in: pages.map(p => p._id) } }, { grantedGroup: transferToUserGroupId });
     await this.updateMany({ _id: { $in: pages.map(p => p._id) } }, { grantedGroup: transferToUserGroupId });
   };
   };
 
 
-  /**
-   * associate GROWI page and HackMD page
-   * @param {Page} pageData
-   * @param {string} pageIdOnHackmd
-   */
-  pageSchema.statics.registerHackmdPage = function(pageData, pageIdOnHackmd) {
-    pageData.pageIdOnHackmd = pageIdOnHackmd;
-    return this.syncRevisionToHackmd(pageData);
-  };
-
-  /**
-   * update revisionHackmdSynced
-   * @param {Page} pageData
-   * @param {bool} isSave whether save or not
-   */
-  pageSchema.statics.syncRevisionToHackmd = function(pageData, isSave = true) {
-    pageData.revisionHackmdSynced = pageData.revision;
-    pageData.hasDraftOnHackmd = false;
-
-    let returnData = pageData;
-    if (isSave) {
-      returnData = pageData.save();
-    }
-    return returnData;
-  };
-
-  /**
-   * update hasDraftOnHackmd
-   * !! This will be invoked many time from many people !!
-   *
-   * @param {Page} pageData
-   * @param {Boolean} newValue
-   */
-  pageSchema.statics.updateHasDraftOnHackmd = async function(pageData, newValue) {
-    if (pageData.hasDraftOnHackmd === newValue) {
-      // do nothing when hasDraftOnHackmd equals to newValue
-      return;
-    }
-
-    pageData.hasDraftOnHackmd = newValue;
-    return pageData.save();
-  };
-
   pageSchema.methods.getNotificationTargetUsers = async function() {
   pageSchema.methods.getNotificationTargetUsers = async function() {
     const Comment = mongoose.model('Comment');
     const Comment = mongoose.model('Comment');
     const Revision = mongoose.model('Revision');
     const Revision = mongoose.model('Revision');

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

@@ -74,7 +74,6 @@ const optionsSchemaForResuming = new Schema<IOptionsForResuming>({
   grant: { type: Number },
   grant: { type: Number },
   grantUserGroupId: { type: ObjectId, ref: 'UserGroup' },
   grantUserGroupId: { type: ObjectId, ref: 'UserGroup' },
   format: { type: String },
   format: { type: String },
-  isSyncRevisionToHackmd: { type: Boolean },
   overwriteScopesOfDescendants: { type: Boolean },
   overwriteScopesOfDescendants: { type: Boolean },
 }, { _id: false });
 }, { _id: false });
 
 

+ 0 - 3
apps/app/src/server/models/page.ts

@@ -101,9 +101,6 @@ const schema = new Schema<PageDocument, PageModel>({
   liker: [{ type: ObjectId, ref: 'User' }],
   liker: [{ type: ObjectId, ref: 'User' }],
   seenUsers: [{ type: ObjectId, ref: 'User' }],
   seenUsers: [{ type: ObjectId, ref: 'User' }],
   commentCount: { type: Number, default: 0 },
   commentCount: { type: Number, default: 0 },
-  pageIdOnHackmd: { type: String },
-  revisionHackmdSynced: { type: ObjectId, ref: 'Revision' }, // the revision that is synced to HackMD
-  hasDraftOnHackmd: { type: Boolean }, // set true if revision and revisionHackmdSynced are same but HackMD document has modified
   expandContentWidth: { type: Boolean },
   expandContentWidth: { type: Boolean },
   updatedAt: { type: Date, default: Date.now }, // Do not use timetamps for updatedAt because it breaks 'updateMetadata: false' option
   updatedAt: { type: Date, default: Date.now }, // Do not use timetamps for updatedAt because it breaks 'updateMetadata: false' option
   deleteUser: { type: ObjectId, ref: 'User' },
   deleteUser: { type: ObjectId, ref: 'User' },

+ 1 - 2
apps/app/src/server/models/serializers/page-serializer.js

@@ -28,9 +28,8 @@ function serializePageSecurely(page) {
     serialized = page.toObject();
     serialized = page.toObject();
   }
   }
 
 
-  // depopulate revision and revisionHackmdSynced
+  // depopulate revision
   depopulate(serialized, 'revision');
   depopulate(serialized, 'revision');
-  depopulate(serialized, 'revisionHackmdSynced');
 
 
   serializeInsecureUserAttributes(serialized);
   serializeInsecureUserAttributes(serialized);
 
 

+ 1 - 3
apps/app/src/server/models/vo/s2c-message.js

@@ -10,15 +10,13 @@ class S2cMessagePageUpdated {
     const serializedPage = serializePageSecurely(page);
     const serializedPage = serializePageSecurely(page);
 
 
     const {
     const {
-      _id, revision, updatedAt, revisionHackmdSynced, hasDraftOnHackmd,
+      _id, revision, updatedAt,
     } = serializedPage;
     } = serializedPage;
 
 
     this.pageId = _id;
     this.pageId = _id;
     this.revisionId = revision;
     this.revisionId = revision;
     this.revisionBody = page.revision.body;
     this.revisionBody = page.revision.body;
     this.revisionUpdateAt = updatedAt;
     this.revisionUpdateAt = updatedAt;
-    this.revisionIdHackmdSynced = revisionHackmdSynced;
-    this.hasDraftOnHackmd = hasDraftOnHackmd;
 
 
     if (user != null) {
     if (user != null) {
       this.remoteLastUpdateUser = user;
       this.remoteLastUpdateUser = user;

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

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

+ 0 - 6
apps/app/src/server/routes/apiv3/overwrite-params/pages.js

@@ -54,12 +54,6 @@ class PageOverwriteParamsFactory {
       params.extended = {};
       params.extended = {};
     }
     }
 
 
-    if (option.initHackmdDatas) {
-      params.pageIdOnHackmd = undefined;
-      params.revisionHackmdSynced = undefined;
-      params.hasDraftOnHackmd = undefined;
-    }
-
     return params;
     return params;
   }
   }
 
 

+ 0 - 7
apps/app/src/server/routes/index.js

@@ -47,7 +47,6 @@ module.exports = function(crowi, app) {
   const comment = require('./comment')(crowi, app);
   const comment = require('./comment')(crowi, app);
   const tag = require('./tag')(crowi, app);
   const tag = require('./tag')(crowi, app);
   const search = require('./search')(crowi, app);
   const search = require('./search')(crowi, app);
-  const hackmd = require('./hackmd')(crowi, app);
   const ogp = require('./ogp')(crowi);
   const ogp = require('./ogp')(crowi);
 
 
   const next = nextFactory(crowi);
   const next = nextFactory(crowi);
@@ -163,12 +162,6 @@ module.exports = function(crowi, app) {
 
 
   app.get('/_search'                            , loginRequired, next.delegateToNext);
   app.get('/_search'                            , loginRequired, next.delegateToNext);
 
 
-  app.get('/_hackmd/load-agent'          , hackmd.loadAgent);
-  app.get('/_hackmd/load-styles'         , hackmd.loadStyles);
-  app.post('/_api/hackmd.integrate'      , accessTokenParser , loginRequiredStrictly , excludeReadOnlyUser, hackmd.validateForApi, hackmd.integrate);
-  app.post('/_api/hackmd.discard'        , accessTokenParser , loginRequiredStrictly , excludeReadOnlyUser, hackmd.validateForApi, hackmd.discard);
-  app.post('/_api/hackmd.saveOnHackmd'   , accessTokenParser , loginRequiredStrictly , excludeReadOnlyUser, hackmd.validateForApi, hackmd.saveOnHackmd);
-
   app.use('/forgot-password', express.Router()
   app.use('/forgot-password', express.Router()
     .use(forgotPassword.checkForgotPasswordEnabledMiddlewareFactory(crowi))
     .use(forgotPassword.checkForgotPasswordEnabledMiddlewareFactory(crowi))
     .get('/', forgotPassword.renderForgotPassword(crowi))
     .get('/', forgotPassword.renderForgotPassword(crowi))

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

@@ -455,7 +455,6 @@ module.exports = function(crowi, app) {
     const overwriteScopesOfDescendants = req.body.overwriteScopesOfDescendants || null;
     const overwriteScopesOfDescendants = req.body.overwriteScopesOfDescendants || null;
     const isSlackEnabled = !!req.body.isSlackEnabled; // cast to boolean
     const isSlackEnabled = !!req.body.isSlackEnabled; // cast to boolean
     const slackChannels = req.body.slackChannels || null;
     const slackChannels = req.body.slackChannels || null;
-    const isSyncRevisionToHackmd = !!req.body.isSyncRevisionToHackmd; // cast to boolean
     const pageTags = req.body.pageTags || undefined;
     const pageTags = req.body.pageTags || undefined;
 
 
     if (pageId === null || pageBody === null || revisionId === null) {
     if (pageId === null || pageBody === null || revisionId === null) {
@@ -482,7 +481,7 @@ module.exports = function(crowi, app) {
       return res.json(ApiResponse.error('Posted param "revisionId" is outdated.', 'conflict', returnLatestRevision));
       return res.json(ApiResponse.error('Posted param "revisionId" is outdated.', 'conflict', returnLatestRevision));
     }
     }
 
 
-    const options = { isSyncRevisionToHackmd, overwriteScopesOfDescendants };
+    const options = { overwriteScopesOfDescendants };
     if (grant != null) {
     if (grant != null) {
       options.grant = grant;
       options.grant = grant;
       options.grantUserGroupId = grantUserGroupId;
       options.grantUserGroupId = grantUserGroupId;

+ 0 - 12
apps/app/src/server/service/config-loader.ts

@@ -61,18 +61,6 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
     type:    ValueType.BOOLEAN,
     type:    ValueType.BOOLEAN,
     default: false,
     default: false,
   },
   },
-  HACKMD_URI: {
-    ns:      'crowi',
-    key:     'app:hackmdUri',
-    type:    ValueType.STRING,
-    default: null,
-  },
-  HACKMD_URI_FOR_SERVER: {
-    ns:      'crowi',
-    key:     'app:hackmdUriForServer',
-    type:    ValueType.STRING,
-    default: null,
-  },
   // OAUTH_GOOGLE_CLIENT_ID: {
   // OAUTH_GOOGLE_CLIENT_ID: {
   //   ns:      'crowi',
   //   ns:      'crowi',
   //   key:     'security:passport-google:clientId',
   //   key:     'security:passport-google:clientId',

+ 7 - 18
apps/app/src/server/service/page.ts

@@ -20,11 +20,11 @@ import {
   PageDeleteConfigValue, IPageDeleteConfigValueToProcessValidation,
   PageDeleteConfigValue, IPageDeleteConfigValueToProcessValidation,
 } from '~/interfaces/page-delete-config';
 } from '~/interfaces/page-delete-config';
 import {
 import {
-  IPageOperationProcessInfo, IPageOperationProcessData, PageActionStage, PageActionType,
+  type IPageOperationProcessInfo, type IPageOperationProcessData, PageActionStage, PageActionType,
 } from '~/interfaces/page-operation';
 } from '~/interfaces/page-operation';
-import { PageMigrationErrorData, SocketEventName, UpdateDescCountRawData } from '~/interfaces/websocket';
+import { SocketEventName, type PageMigrationErrorData, type UpdateDescCountRawData } from '~/interfaces/websocket';
 import {
 import {
-  CreateMethod, PageCreateOptions, PageModel, PageDocument, pushRevision, PageQueryBuilder,
+  type CreateMethod, type PageCreateOptions, type PageModel, type PageDocument, pushRevision, PageQueryBuilder,
 } from '~/server/models/page';
 } from '~/server/models/page';
 import { createBatchStream } from '~/server/util/batch-stream';
 import { createBatchStream } from '~/server/util/batch-stream';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
@@ -32,9 +32,9 @@ import { prepareDeleteConfigValuesForCalc } from '~/utils/page-delete-config';
 
 
 import { ObjectIdLike } from '../interfaces/mongoose-utils';
 import { ObjectIdLike } from '../interfaces/mongoose-utils';
 import { PathAlreadyExistsError } from '../models/errors';
 import { PathAlreadyExistsError } from '../models/errors';
-import { IOptionsForCreate, IOptionsForUpdate } from '../models/interfaces/page-operation';
-import PageOperation, { PageOperationDocument } from '../models/page-operation';
-import { PageRedirectModel } from '../models/page-redirect';
+import type { IOptionsForCreate, IOptionsForUpdate } from '../models/interfaces/page-operation';
+import PageOperation, { type PageOperationDocument } from '../models/page-operation';
+import type { PageRedirectModel } from '../models/page-redirect';
 import { serializePageSecurely } from '../models/serializers/page-serializer';
 import { serializePageSecurely } from '../models/serializers/page-serializer';
 import Subscription from '../models/subscription';
 import Subscription from '../models/subscription';
 import { V5ConversionError } from '../models/vo/v5-conversion-error';
 import { V5ConversionError } from '../models/vo/v5-conversion-error';
@@ -3882,10 +3882,9 @@ class PageService {
   async updateGrant(page, user, grantData: {grant: PageGrant, grantedGroup: ObjectIdLike}): Promise<PageDocument> {
   async updateGrant(page, user, grantData: {grant: PageGrant, grantedGroup: ObjectIdLike}): Promise<PageDocument> {
     const { grant, grantedGroup } = grantData;
     const { grant, grantedGroup } = grantData;
 
 
-    const options = {
+    const options: IOptionsForUpdate = {
       grant,
       grant,
       grantUserGroupId: grantedGroup,
       grantUserGroupId: grantedGroup,
-      isSyncRevisionToHackmd: false,
     };
     };
 
 
     return this.updatePage(page, null, null, user, options);
     return this.updatePage(page, null, null, user, options);
@@ -4011,17 +4010,12 @@ class PageService {
     let savedPage = await newPageData.save();
     let savedPage = await newPageData.save();
 
 
     // Update body
     // Update body
-    const isSyncRevisionToHackmd = options.isSyncRevisionToHackmd;
     const isBodyPresent = body != null && previousBody != null;
     const isBodyPresent = body != null && previousBody != null;
     const shouldUpdateBody = isBodyPresent;
     const shouldUpdateBody = isBodyPresent;
     if (shouldUpdateBody) {
     if (shouldUpdateBody) {
       const newRevision = await Revision.prepareRevision(newPageData, body, previousBody, user);
       const newRevision = await Revision.prepareRevision(newPageData, body, previousBody, user);
       savedPage = await pushRevision(savedPage, newRevision, user);
       savedPage = await pushRevision(savedPage, newRevision, user);
       await savedPage.populateDataToShowRevision();
       await savedPage.populateDataToShowRevision();
-
-      if (isSyncRevisionToHackmd) {
-        savedPage = await Page.syncRevisionToHackmd(savedPage);
-      }
     }
     }
 
 
 
 
@@ -4074,7 +4068,6 @@ class PageService {
 
 
     const grant = options.grant || pageData.grant; // use the previous data if absence
     const grant = options.grant || pageData.grant; // use the previous data if absence
     const grantUserGroupId = options.grantUserGroupId || pageData.grantUserGroupId; // use the previous data if absence
     const grantUserGroupId = options.grantUserGroupId || pageData.grantUserGroupId; // use the previous data if absence
-    const isSyncRevisionToHackmd = options.isSyncRevisionToHackmd;
 
 
     await this.validateAppliedScope(user, grant, grantUserGroupId);
     await this.validateAppliedScope(user, grant, grantUserGroupId);
     pageData.applyScope(user, grant, grantUserGroupId);
     pageData.applyScope(user, grant, grantUserGroupId);
@@ -4089,10 +4082,6 @@ class PageService {
       const newRevision = await Revision.prepareRevision(pageData, body, previousBody, user);
       const newRevision = await Revision.prepareRevision(pageData, body, previousBody, user);
       savedPage = await pushRevision(savedPage, newRevision, user);
       savedPage = await pushRevision(savedPage, newRevision, user);
       await savedPage.populateDataToShowRevision();
       await savedPage.populateDataToShowRevision();
-
-      if (isSyncRevisionToHackmd) {
-        savedPage = await Page.syncRevisionToHackmd(savedPage);
-      }
     }
     }
 
 
     // update scopes for descendants
     // update scopes for descendants

+ 3 - 15
apps/app/src/server/service/system-events/sync-page-status.ts

@@ -1,11 +1,10 @@
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-import S2sMessage from '../../models/vo/s2s-message';
 import { S2cMessagePageUpdated } from '../../models/vo/s2c-message';
 import { S2cMessagePageUpdated } from '../../models/vo/s2c-message';
-import { S2sMessageHandlable } from '../s2s-messaging/handlable';
-import { S2sMessagingService } from '../s2s-messaging/base';
-
+import S2sMessage from '../../models/vo/s2s-message';
 import { RoomPrefix, getRoomNameWithId } from '../../util/socket-io-helpers';
 import { RoomPrefix, getRoomNameWithId } from '../../util/socket-io-helpers';
+import { S2sMessagingService } from '../s2s-messaging/base';
+import { S2sMessageHandlable } from '../s2s-messaging/handlable';
 
 
 const logger = loggerFactory('growi:service:system-events:SyncPageStatusService');
 const logger = loggerFactory('growi:service:system-events:SyncPageStatusService');
 
 
@@ -125,17 +124,6 @@ class SyncPageStatusService implements S2sMessageHandlable {
 
 
       this.publishToOtherServers('page:delete', { s2cMessagePageUpdated });
       this.publishToOtherServers('page:delete', { s2cMessagePageUpdated });
     });
     });
-    this.emitter.on('saveOnHackmd', (page, user) => {
-      const s2cMessagePageUpdated = new S2cMessagePageUpdated(page);
-
-      // emit to the room for each page
-      socketIoService.getDefaultSocket()
-        .in(getRoomNameWithId(RoomPrefix.PAGE, page._id))
-        .except(getRoomNameWithId(RoomPrefix.USER, user._id))
-        .emit('page:editingWithHackmd', { s2cMessagePageUpdated });
-
-      this.publishToOtherServers('page:editingWithHackmd', { s2cMessagePageUpdated });
-    });
   }
   }
 
 
 }
 }

+ 0 - 4
apps/app/src/stores/context.tsx

@@ -72,10 +72,6 @@ export const useRegistrationWhitelist = (initialData?: Nullable<string[]>): SWRR
   return useContextSWR<Nullable<string[]>, Error>('registrationWhitelist', initialData);
   return useContextSWR<Nullable<string[]>, Error>('registrationWhitelist', initialData);
 };
 };
 
 
-export const useHackmdUri = (initialData?: Nullable<string>): SWRResponse<Nullable<string>, Error> => {
-  return useContextSWR<Nullable<string>, Error>('hackmdUri', initialData);
-};
-
 export const useIsSearchPage = (initialData?: Nullable<boolean>) : SWRResponse<Nullable<boolean>, Error> => {
 export const useIsSearchPage = (initialData?: Nullable<boolean>) : SWRResponse<Nullable<boolean>, Error> => {
   return useContextSWR<Nullable<any>, Error>('isSearchPage', initialData);
   return useContextSWR<Nullable<any>, Error>('isSearchPage', initialData);
 };
 };

+ 26 - 21
apps/app/src/stores/page.tsx

@@ -7,6 +7,7 @@ import type {
   IPageInfo, IPageInfoForOperation,
   IPageInfo, IPageInfoForOperation,
   IRevision, IRevisionHasId,
   IRevision, IRevisionHasId,
 } from '@growi/core';
 } from '@growi/core';
+import { useSWRStatic } from '@growi/core/dist/swr';
 import { isClient, pagePathUtils } from '@growi/core/dist/utils';
 import { isClient, pagePathUtils } from '@growi/core/dist/utils';
 import useSWR, {
 import useSWR, {
   mutate, useSWRConfig, type SWRResponse, type SWRConfiguration,
   mutate, useSWRConfig, type SWRResponse, type SWRConfiguration,
@@ -18,35 +19,36 @@ import useSWRMutation, { type SWRMutationResponse } from 'swr/mutation';
 import { apiGet } from '~/client/util/apiv1-client';
 import { apiGet } from '~/client/util/apiv1-client';
 import { apiv3Get } from '~/client/util/apiv3-client';
 import { apiv3Get } from '~/client/util/apiv3-client';
 import type { IRecordApplicableGrant, IResIsGrantNormalized } from '~/interfaces/page-grant';
 import type { IRecordApplicableGrant, IResIsGrantNormalized } from '~/interfaces/page-grant';
+import type { AxiosResponse } from '~/utils/axios';
 
 
 import type { IPageTagsInfo } from '../interfaces/tag';
 import type { IPageTagsInfo } from '../interfaces/tag';
 
 
 import {
 import {
   useCurrentPathname, useShareLinkId, useIsGuestUser, useIsReadOnlyUser,
   useCurrentPathname, useShareLinkId, useIsGuestUser, useIsReadOnlyUser,
 } from './context';
 } from './context';
-import { useStaticSWR } from './use-static-swr';
+
 
 
 const { isPermalink: _isPermalink } = pagePathUtils;
 const { isPermalink: _isPermalink } = pagePathUtils;
 
 
 
 
 export const useCurrentPageId = (initialData?: Nullable<string>): SWRResponse<Nullable<string>, Error> => {
 export const useCurrentPageId = (initialData?: Nullable<string>): SWRResponse<Nullable<string>, Error> => {
-  return useStaticSWR<Nullable<string>, Error>('currentPageId', initialData);
+  return useSWRStatic<Nullable<string>, Error>('currentPageId', initialData);
 };
 };
 
 
 export const useIsLatestRevision = (initialData?: boolean): SWRResponse<boolean, any> => {
 export const useIsLatestRevision = (initialData?: boolean): SWRResponse<boolean, any> => {
-  return useStaticSWR('isLatestRevision', initialData);
+  return useSWRStatic('isLatestRevision', initialData);
 };
 };
 
 
 export const useIsNotFound = (initialData?: boolean): SWRResponse<boolean, Error> => {
 export const useIsNotFound = (initialData?: boolean): SWRResponse<boolean, Error> => {
-  return useStaticSWR<boolean, Error>('isNotFound', initialData, { fallbackData: false });
+  return useSWRStatic<boolean, Error>('isNotFound', initialData, { fallbackData: false });
 };
 };
 
 
 export const useTemplateTagData = (initialData?: string[]): SWRResponse<string[], Error> => {
 export const useTemplateTagData = (initialData?: string[]): SWRResponse<string[], Error> => {
-  return useStaticSWR<string[], Error>('templateTagData', initialData);
+  return useSWRStatic<string[], Error>('templateTagData', initialData);
 };
 };
 
 
 export const useTemplateBodyData = (initialData?: string): SWRResponse<string, Error> => {
 export const useTemplateBodyData = (initialData?: string): SWRResponse<string, Error> => {
-  return useStaticSWR<string, Error>('templateBodyData', initialData);
+  return useSWRStatic<string, Error>('templateBodyData', initialData);
 };
 };
 
 
 /** "useSWRxCurrentPage" is intended for initial data retrieval only. Use "useSWRMUTxCurrentPage" for revalidation */
 /** "useSWRxCurrentPage" is intended for initial data retrieval only. Use "useSWRMUTxCurrentPage" for revalidation */
@@ -71,6 +73,17 @@ export const useSWRxCurrentPage = (initialData?: IPagePopulatedToShowRevision|nu
   });
   });
 };
 };
 
 
+const getPageApiErrorHandler = (errs: AxiosResponse[]) => {
+  if (!Array.isArray(errs)) { throw Error('error is not array') }
+
+  const statusCode = errs[0].status;
+  if (statusCode === 403 || statusCode === 404) {
+    // for NotFoundPage
+    return null;
+  }
+  throw Error('failed to get page');
+};
+
 export const useSWRMUTxCurrentPage = (): SWRMutationResponse<IPagePopulatedToShowRevision|null> => {
 export const useSWRMUTxCurrentPage = (): SWRMutationResponse<IPagePopulatedToShowRevision|null> => {
   const key = 'currentPage';
   const key = 'currentPage';
 
 
@@ -87,19 +100,9 @@ export const useSWRMUTxCurrentPage = (): SWRMutationResponse<IPagePopulatedToSho
 
 
   return useSWRMutation(
   return useSWRMutation(
     key,
     key,
-    async() => {
-      return apiv3Get<{ page: IPagePopulatedToShowRevision }>('/page', { pageId: currentPageId, shareLinkId, revisionId })
-        .then(result => result.data.page)
-        .catch((errs) => {
-          if (!Array.isArray(errs)) { throw Error('error is not array') }
-          const statusCode = errs[0].status;
-          if (statusCode === 403 || statusCode === 404) {
-            // for NotFoundPage
-            return null;
-          }
-          throw Error('failed to get page');
-        });
-    },
+    () => apiv3Get<{ page: IPagePopulatedToShowRevision }>('/page', { pageId: currentPageId, shareLinkId, revisionId })
+      .then(result => result.data.page)
+      .catch(getPageApiErrorHandler),
     {
     {
       populateCache: true,
       populateCache: true,
       revalidate: false,
       revalidate: false,
@@ -107,10 +110,12 @@ export const useSWRMUTxCurrentPage = (): SWRMutationResponse<IPagePopulatedToSho
   );
   );
 };
 };
 
 
-export const useSWRxPageByPath = (path?: string, config?: SWRConfiguration): SWRResponse<IPagePopulatedToShowRevision, Error> => {
+export const useSWRxPageByPath = (path?: string, config?: SWRConfiguration): SWRResponse<IPagePopulatedToShowRevision|null, Error> => {
   return useSWR(
   return useSWR(
     path != null ? ['/page', path] : null,
     path != null ? ['/page', path] : null,
-    ([endpoint, path]) => apiv3Get<{ page: IPagePopulatedToShowRevision }>(endpoint, { path }).then(result => result.data.page),
+    ([endpoint, path]) => apiv3Get<{ page: IPagePopulatedToShowRevision }>(endpoint, { path })
+      .then(result => result.data.page)
+      .catch(getPageApiErrorHandler),
     {
     {
       ...config,
       ...config,
       keepPreviousData: true,
       keepPreviousData: true,

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