Kaynağa Gözat

Merge branch 'dev/7.0.x' into imprv/126528-128886-attach-some-files

reiji-h 2 yıl önce
ebeveyn
işleme
6551122c68
80 değiştirilmiş dosya ile 426 ekleme ve 709 silme
  1. 1 2
      README.md
  2. 1 2
      README_JP.md
  3. 1 0
      _obsolete/packages/.eslintignore
  4. 0 0
      _obsolete/packages/hackmd/.eslintignore
  5. 0 0
      _obsolete/packages/hackmd/.gitignore
  6. 0 0
      _obsolete/packages/hackmd/package.json
  7. 0 0
      _obsolete/packages/hackmd/src/hackmd-agent.js
  8. 0 0
      _obsolete/packages/hackmd/src/hackmd-styles.ts
  9. 0 0
      _obsolete/packages/hackmd/src/index.ts
  10. 0 0
      _obsolete/packages/hackmd/src/style.scss
  11. 0 0
      _obsolete/packages/hackmd/tsconfig.json
  12. 0 0
      _obsolete/packages/hackmd/vite.config.js
  13. 0 2
      apps/app/.env.development
  14. 0 0
      apps/app/_obsolete/src/client/services/side-effects/hackmd-draft-updated.ts
  15. 0 0
      apps/app/_obsolete/src/components/PageEditorByHackmd.tsx
  16. 0 0
      apps/app/_obsolete/src/components/PageEditorByHackmd/HackmdEditor.jsx
  17. 0 0
      apps/app/_obsolete/src/interfaces/hackmd.ts
  18. 0 0
      apps/app/_obsolete/src/server/routes/hackmd.js
  19. 0 0
      apps/app/_obsolete/src/stores/hackmd.ts
  20. 0 2
      apps/app/package.json
  21. 0 4
      apps/app/public/static/locales/en_US/admin.json
  22. 1 22
      apps/app/public/static/locales/en_US/translation.json
  23. 0 4
      apps/app/public/static/locales/ja_JP/admin.json
  24. 1 22
      apps/app/public/static/locales/ja_JP/translation.json
  25. 0 4
      apps/app/public/static/locales/zh_CN/admin.json
  26. 1 22
      apps/app/public/static/locales/zh_CN/translation.json
  27. 0 19
      apps/app/src/client/services/page-operation.ts
  28. 2 4
      apps/app/src/client/services/side-effects/page-updated.ts
  29. 13 15
      apps/app/src/components/Admin/Common/Accordion.jsx
  30. 0 13
      apps/app/src/components/Admin/ImportData/GrowiArchive/ImportCollectionConfigurationModal.jsx
  31. 14 18
      apps/app/src/components/Admin/Security/SamlSecuritySettingContents.jsx
  32. 6 6
      apps/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx
  33. 12 12
      apps/app/src/components/Admin/SlackIntegration/WithProxyAccordions.jsx
  34. 3 3
      apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  35. 0 127
      apps/app/src/components/Navbar/PageEditorModeManager.jsx
  36. 50 24
      apps/app/src/components/Navbar/PageEditorModeManager.module.scss
  37. 103 0
      apps/app/src/components/Navbar/PageEditorModeManager.tsx
  38. 0 2
      apps/app/src/components/Page/DisplaySwitcher.tsx
  39. 3 4
      apps/app/src/components/PageEditor/EditorNavbarBottom.tsx
  40. 0 2
      apps/app/src/components/PageEditor/PageEditor.tsx
  41. 19 61
      apps/app/src/components/PageStatusAlert.tsx
  42. 3 1
      apps/app/src/components/ReactMarkdownComponents/LightBox.tsx
  43. 4 15
      apps/app/src/components/Sidebar/PageTree/Item.module.scss
  44. 1 6
      apps/app/src/components/Sidebar/Sidebar.module.scss
  45. 14 0
      apps/app/src/components/Sidebar/SidebarContents.module.scss
  46. 6 1
      apps/app/src/components/Sidebar/SidebarContents.tsx
  47. 1 1
      apps/app/src/components/Sidebar/SidebarNav.module.scss
  48. 0 1
      apps/app/src/interfaces/page-operation.ts
  49. 0 3
      apps/app/src/interfaces/websocket.ts
  50. 0 1
      apps/app/src/models/admin/import-option-for-pages.js
  51. 2 10
      apps/app/src/pages/[[...path]].page.tsx
  52. 0 1
      apps/app/src/server/models/interfaces/page-operation.ts
  53. 0 43
      apps/app/src/server/models/obsolete-page.js
  54. 0 1
      apps/app/src/server/models/page-operation.ts
  55. 0 3
      apps/app/src/server/models/page.ts
  56. 1 2
      apps/app/src/server/models/serializers/page-serializer.js
  57. 1 3
      apps/app/src/server/models/vo/s2c-message.js
  58. 0 6
      apps/app/src/server/routes/apiv3/overwrite-params/pages.js
  59. 0 7
      apps/app/src/server/routes/index.js
  60. 1 2
      apps/app/src/server/routes/page.js
  61. 0 12
      apps/app/src/server/service/config-loader.ts
  62. 7 18
      apps/app/src/server/service/page.ts
  63. 3 15
      apps/app/src/server/service/system-events/sync-page-status.ts
  64. 0 4
      apps/app/src/stores/context.tsx
  65. 9 19
      apps/app/src/stores/remote-latest-page.ts
  66. 0 10
      apps/app/src/stores/ui.tsx
  67. 0 48
      apps/app/src/styles/_editor.scss
  68. 14 0
      apps/app/src/styles/molecules/_list-group-item.scss
  69. 8 7
      apps/app/src/styles/organisms/_wiki.scss
  70. 1 0
      apps/app/src/styles/style-app.scss
  71. 0 1
      apps/app/tsconfig.build.server.json
  72. 13 9
      packages/core/scss/bootstrap/theming/_variables.scss
  73. 28 28
      packages/core/scss/bootstrap/theming/utils/_color-palette.scss
  74. 0 3
      packages/core/src/interfaces/page.ts
  75. 0 12
      packages/editor/src/components/CodeMirrorEditor/CodeMirrorEditor.tsx
  76. 65 0
      packages/editor/src/components/CodeMirrorEditorComment.tsx
  77. 1 0
      packages/editor/src/consts/global-code-mirror-editor-key.ts
  78. 3 2
      packages/editor/src/services/codemirror-editor/use-codemirror-editor/use-codemirror-editor.ts
  79. 9 11
      packages/preset-themes/src/styles/mono-blue.scss
  80. 0 7
      yarn.lock

+ 1 - 2
README.md

@@ -38,8 +38,7 @@
 
 - **Features**
   - 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
   - SSO(Single Sign On) with SAML
   - 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)。
-  - [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 認証をサポートしています。
   - SAML を用いた Single Sign On が可能です。
   - 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_REQUEST_TIMEOUT=15000
 ELASTICSEARCH_REJECT_UNAUTHORIZED=true
-HACKMD_URI="http://localhost:3010"
-HACKMD_URI_FOR_SERVER="http://hackmd:3000"
 OGP_URI="http://ogp:8088"
 QUESTIONNAIRE_SERVER_ORIGIN="http://host.docker.internal:3003"
 # 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 - 2
apps/app/package.json

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

+ 0 - 4
apps/app/public/static/locales/en_US/admin.json

@@ -557,10 +557,6 @@
           "initialize_meta_datas": {
             "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."
-          },
-          "initialize_hackmd_related_datas": {
-            "label": "Initialize HackMD related data",
-            "desc": "Recommended to check this unless there is important drafts on HackMD."
           }
         },
         "revisions": {

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

@@ -502,27 +502,6 @@
     "insert_image": "inserts an 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": "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": {
     "popover_title": "Slack Notification",
     "popover_desc": "Input channel name. You can notify multiple channels by entering a comma-separated list."
@@ -737,7 +716,7 @@
         "isForbidden": "Authority not allowed to view",
         "currentPageGrantLabel": "Authorization for this 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": {
         "restrected": "Only those who know the link",

+ 0 - 4
apps/app/public/static/locales/ja_JP/admin.json

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

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

@@ -535,27 +535,6 @@
     "insert_image": "で画像を挿入できます",
     "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": {
     "popover_title": "Slack 通知",
     "popover_desc": "チャンネル名を入れてください。カンマ区切りのリストを入力することで複数のチャンネルに通知することができます。"
@@ -770,7 +749,7 @@
         "isForbidden": "権限の閲覧が許可されていません",
         "currentPageGrantLabel": "このページの権限: ",
         "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": {
         "restrected": "リンクを知っている人のみ",

+ 0 - 4
apps/app/public/static/locales/zh_CN/admin.json

@@ -565,10 +565,6 @@
           "initialize_meta_datas": {
             "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."
-          },
-          "initialize_hackmd_related_datas": {
-            "label": "Initialize HackMD related data",
-            "desc": "Recommended to check this unless there is important drafts on HackMD."
           }
         },
         "revisions": {

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

@@ -489,27 +489,6 @@
 		"insert_image": "插入图像",
 		"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": {
     "popover_title": "Slack Notification",
     "popover_desc": "Input channel name. You can notify multiple channels by entering a comma-separated list."
@@ -740,7 +719,7 @@
         "isForbidden": "无权查看的机构",
         "currentPageGrantLabel": "本页的权限: ",
         "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": {
         "restrected": "只有那些知道链接的人",

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

@@ -135,23 +135,6 @@ export const useSaveOrUpdate = (): SaveOrUpdateFunction => {
     const { path, pageId, revisionId } = pageInfo;
 
     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;
     if (pageId == null || revisionId == null) {
@@ -209,8 +192,6 @@ export const useUpdateStateAfterSave = (pageId: string|undefined|null, opts?: Up
       remoteRevisionBody: updatedPage.revision.body,
       remoteRevisionLastUpdateUser: updatedPage.lastUpdateUser,
       remoteRevisionLastUpdatedAt: updatedPage.updatedAt,
-      revisionIdHackmdSynced: updatedPage.revisionHackmdSynced?.toString(),
-      hasDraftOnHackmd: updatedPage.hasDraftOnHackmd,
     };
 
     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 { 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';
 
 export const usePageUpdatedEffect = (): void => {
@@ -15,13 +15,11 @@ export const usePageUpdatedEffect = (): void => {
   const setLatestRemotePageData = useCallback((data) => {
     const { s2cMessagePageUpdated } = data;
 
-    const remoteData = {
+    const remoteData: RemoteRevisionData = {
       remoteRevisionId: s2cMessagePageUpdated.revisionId,
       remoteRevisionBody: s2cMessagePageUpdated.revisionBody,
       remoteRevisionLastUpdateUser: s2cMessagePageUpdated.remoteLastUpdateUser,
       remoteRevisionLastUpdatedAt: s2cMessagePageUpdated.revisionUpdateAt,
-      revisionIdHackmdSynced: s2cMessagePageUpdated.revisionIdHackmdSynced,
-      hasDraftOnHackmd: s2cMessagePageUpdated.hasDraftOnHackmd,
     };
 
     if (currentPageId != null && currentPageId === s2cMessagePageUpdated.pageId) {

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

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

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

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

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

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

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

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

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

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

@@ -52,7 +52,7 @@ const AuthorInfoSkeleton = () => <Skeleton additionalClass={`${AuthorInfoStyles[
 
 
 const PageEditorModeManager = dynamic(
-  () => import('./PageEditorModeManager'),
+  () => import('./PageEditorModeManager').then(mod => mod.PageEditorModeManager),
   { 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
@@ -395,9 +395,9 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
             )}
             {isAbleToChangeEditorMode && (
               <PageEditorModeManager
-                onPageEditorModeButtonClicked={viewType => mutateEditorMode(viewType)}
-                isBtnDisabled={!!isGuestUser || !!isReadOnlyUser}
                 editorMode={editorMode}
+                isBtnDisabled={!!isGuestUser || !!isReadOnlyUser}
+                onPageEditorModeButtonClicked={viewType => mutateEditorMode(viewType)}
               />
             )}
           </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 '~/styles/mixins';
 
-$btn-line-height: 1.2rem;
-
 .grw-page-editor-mode-manager :global {
   .btn {
+    --bs-btn-font-size: 13px;
+    --bs-btn-border-width: 2px;
+
     width: 70px;
+
     white-space: nowrap;
 
     @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 {
+  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 { useHackmdDraftUpdatedEffect } from '~/client/services/side-effects/hackmd-draft-updated';
 import { useHashChangedEffect } from '~/client/services/side-effects/hash-changed';
 import { usePageUpdatedEffect } from '~/client/services/side-effects/page-updated';
 import { useIsEditable } from '~/stores/context';
@@ -28,7 +27,6 @@ export const DisplaySwitcher = (props: Props): JSX.Element => {
 
   usePageUpdatedEffect();
   useHashChangedEffect();
-  useHackmdDraftUpdatedEffect();
 
   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 { useCurrentPagePath } from '~/stores/page';
 import {
-  EditorMode, useDrawerOpened, useEditorMode, useIsDeviceSmallerThanMd,
+  useDrawerOpened, useEditorMode, useIsDeviceSmallerThanMd,
 } from '~/stores/ui';
 
 
@@ -76,8 +76,7 @@ const EditorNavbarBottom = (): JSX.Element => {
     </div>
   );
 
-  const isOptionsSelectorEnabled = editorMode !== EditorMode.HackMD;
-  const isCollapsedOptionsSelectorEnabled = isOptionsSelectorEnabled && isDeviceSmallerThanMd;
+  const isCollapsedOptionsSelectorEnabled = isDeviceSmallerThanMd;
 
   return (
     <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(' ')}`}>
         <form>
           { isDeviceSmallerThanMd && renderDrawerButton() }
-          { isOptionsSelectorEnabled && !isDeviceSmallerThanMd && <OptionsSelector /> }
+          { !isDeviceSmallerThanMd && <OptionsSelector /> }
         </form>
         <form className="flex-nowrap ms-auto">
           {/* Responsive Design for the SlackNotification */}

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

@@ -347,7 +347,6 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
         // Not using 'mutateGrant' to inherit the grant of the parent page
         if (res.pageCreated) {
           logger.info('Page is created', res.page._id);
-          globalEmitter.emit('resetInitializedHackMdStatus');
           mutateIsLatestRevision(true);
           setCreatedPageRevisionIdWithAttachment(res.page.revision);
           await mutateCurrentPageId(res.page._id);
@@ -360,7 +359,6 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
       }
       finally {
       }
-
     });
 
   }, [codeMirrorEditor, currentPagePath, mutateCurrentPage, mutateCurrentPageId, mutateIsLatestRevision, pageId]);

+ 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 { useEditingMarkdown, useIsConflict } from '~/stores/editor';
-import {
-  useHasDraftOnHackmd, useIsHackmdDraftUpdatingInRealtime, useRevisionIdHackmdSynced,
-} from '~/stores/hackmd';
 import { useConflictDiffModal } from '~/stores/modal';
 import { useSWRMUTxCurrentPage, useSWRxCurrentPage } from '~/stores/page';
 import { useRemoteRevisionId, useRemoteRevisionLastUpdateUser } from '~/stores/remote-latest-page';
-import { EditorMode, useEditorMode } from '~/stores/ui';
 
 import { Username } from './User/Username';
 
@@ -26,17 +22,13 @@ type AlertComponentContents = {
 export const PageStatusAlert = (): JSX.Element => {
 
   const { t } = useTranslation();
-  const { data: isHackmdDraftUpdatingInRealtime } = useIsHackmdDraftUpdatingInRealtime();
-  const { data: hasDraftOnHackmd } = useHasDraftOnHackmd();
   const { data: isConflict } = useIsConflict();
   const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
   const { open: openConflictDiffModal } = useConflictDiffModal();
-  const { mutate: mutateEditorMode } = useEditorMode();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isReadOnlyUser } = useIsReadOnlyUser();
 
   // store remote latest page data
-  const { data: revisionIdHackmdSynced } = useRevisionIdHackmdSynced();
   const { data: remoteRevisionId } = useRemoteRevisionId();
   const { data: remoteRevisionLastUpdateUser } = useRemoteRevisionLastUpdateUser();
 
@@ -53,37 +45,23 @@ export const PageStatusAlert = (): JSX.Element => {
     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 => {
 
@@ -123,37 +101,17 @@ export const PageStatusAlert = (): JSX.Element => {
 
   const alertComponentContents = useMemo(() => {
     const isRevisionOutdated = revision?._id !== remoteRevisionId;
-    const isHackmdDocumentOutdated = revisionIdHackmdSynced !== remoteRevisionId;
 
     // 'revision?._id' and 'remoteRevisionId' are can not be undefined
     if (revision?._id == null || remoteRevisionId == null) { return }
 
     // when remote revision is newer than both
-    if (isHackmdDocumentOutdated && isRevisionOutdated) {
+    if (isRevisionOutdated) {
       return getContentsForUpdatedAlert();
     }
 
-    // when someone editing with HackMD
-    if (isHackmdDraftUpdatingInRealtime) {
-      return getContentsForSomeoneEditingAlert();
-    }
-
-    // when the draft of HackMD is newest
-    if (hasDraftOnHackmd) {
-      return getContentsForDraftExistsAlert();
-    }
-
     return null;
-  }, [
-    revision?._id,
-    remoteRevisionId,
-    revisionIdHackmdSynced,
-    isHackmdDraftUpdatingInRealtime,
-    hasDraftOnHackmd,
-    getContentsForUpdatedAlert,
-    getContentsForSomeoneEditingAlert,
-    getContentsForDraftExistsAlert,
-  ]);
+  }, [revision?._id, remoteRevisionId, getContentsForUpdatedAlert]);
 
   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) => {
   const [toggler, setToggler] = useState(false);
+  const { node, ...rest } = props;
+
   return (
     <>
-      <img {...props.node.properties} onClick={() => setToggler(!toggler)} />
+      <img {...rest} onClick={() => setToggler(!toggler)} />
       <FsLightbox
         toggler={toggler}
         sources={[props.src]}

+ 4 - 15
apps/app/src/components/Sidebar/PageTree/Item.module.scss

@@ -2,21 +2,13 @@
 
 // TODO: relocate following styles into PageTreeItem.mdoule.scss after refactoring
 // https://redmine.weseek.co.jp/issues/127544
-.pagetree-item :global {
-  .list-group-item {
-    --bs-list-group-bg: transparent;
-  }
-}
 @include bs.color-mode(light) {
   .pagetree-item :global {
     .list-group-item-action {
-      --bs-list-group-action-hover-bg: var(--grw-highlight-200);
-      --bs-list-group-action-active-bg: var(--grw-highlight-400);
-
       .btn-page-item-control {
         --bs-btn-bg: transparent;
-        --bs-btn-hover-bg: var(--grw-highlight-400);
-        --bs-btn-active-bg: var(--grw-highlight-600);
+        --bs-btn-hover-bg: var(--grw-primary-300);
+        --bs-btn-active-bg: var(--grw-primary-400);
       }
     }
   }
@@ -24,13 +16,10 @@
 @include bs.color-mode(dark) {
   .pagetree-item :global {
     .list-group-item-action {
-      --bs-list-group-action-hover-bg: var(--grw-highlight-800);
-      --bs-list-group-action-active-bg: var(--grw-highlight-800);
-
       .btn-page-item-control {
         --bs-btn-bg: transparent;
-        --bs-btn-hover-bg: var(--grw-highlight-700);
-        --bs-btn-active-bg: var(--grw-highlight-800);
+        --bs-btn-hover-bg: var(--grw-primary-700);
+        --bs-btn-active-bg: var(--grw-primary-800);
       }
     }
   }

+ 1 - 6
apps/app/src/components/Sidebar/Sidebar.module.scss

@@ -218,11 +218,6 @@
 
 .grw-sidebar :global {
   .grw-contextual-navigation {
-    --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;
-
     backdrop-filter: blur(20px);
   }
 }
@@ -242,7 +237,7 @@
     --bs-border-color: var(--grw-highlight-800);
 
     .grw-contextual-navigation {
-      background-color: rgba(var(--grw-highlight-900-rgb), .5);
+      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 Tag from './Tag';
 
+import styles from './SidebarContents.module.scss';
+
+
 export const SidebarContents = memo(() => {
   const { data: currentSidebarContents } = useCurrentSidebarContents();
 
@@ -31,7 +34,9 @@ export const SidebarContents = memo(() => {
   }
 
   return (
-    <Contents />
+    <div className={`grw-sidebar-contents ${styles['grw-sidebar-contents']}`}>
+      <Contents />
+    </div>
   );
 });
 SidebarContents.displayName = 'SidebarContents';

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

@@ -124,7 +124,7 @@
 }
 @include bs.color-mode(dark) {
   .grw-sidebar-nav :global {
-    background-color: var(--grw-sidebar-nav-bg, var(--grw-highlight-900));
+    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));

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

@@ -34,5 +34,4 @@ export type OptionsToSave = {
   pageTags: string[] | null;
   grantUserGroupId?: 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',
   PageDeleted: 'page:delete',
 
-  // Hackmd
-  EditingWithHackmd: 'page:editingWithHackmd',
-
 } as const;
 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,
   makePublicForGrant5: false,
   initPageMetadatas: false,
-  initHackmdDatas: false,
 };
 
 class ImportOptionForPages extends GrowiArchiveImportOption {

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

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

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

@@ -24,7 +24,6 @@ export type IUserForResuming = {
 export type IOptionsForUpdate = {
   grant?: PageGrant,
   grantUserGroupId?: ObjectIdLike,
-  isSyncRevisionToHackmd?: 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 });
   };
 
-  /**
-   * 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() {
     const Comment = mongoose.model('Comment');
     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 },
   grantUserGroupId: { type: ObjectId, ref: 'UserGroup' },
   format: { type: String },
-  isSyncRevisionToHackmd: { type: Boolean },
   overwriteScopesOfDescendants: { type: Boolean },
 }, { _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' }],
   seenUsers: [{ type: ObjectId, ref: 'User' }],
   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 },
   updatedAt: { type: Date, default: Date.now }, // Do not use timetamps for updatedAt because it breaks 'updateMetadata: false' option
   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();
   }
 
-  // depopulate revision and revisionHackmdSynced
+  // depopulate revision
   depopulate(serialized, 'revision');
-  depopulate(serialized, 'revisionHackmdSynced');
 
   serializeInsecureUserAttributes(serialized);
 

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

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

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

@@ -54,12 +54,6 @@ class PageOverwriteParamsFactory {
       params.extended = {};
     }
 
-    if (option.initHackmdDatas) {
-      params.pageIdOnHackmd = undefined;
-      params.revisionHackmdSynced = undefined;
-      params.hasDraftOnHackmd = undefined;
-    }
-
     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 tag = require('./tag')(crowi, app);
   const search = require('./search')(crowi, app);
-  const hackmd = require('./hackmd')(crowi, app);
   const ogp = require('./ogp')(crowi);
 
   const next = nextFactory(crowi);
@@ -163,12 +162,6 @@ module.exports = function(crowi, app) {
 
   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()
     .use(forgotPassword.checkForgotPasswordEnabledMiddlewareFactory(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 isSlackEnabled = !!req.body.isSlackEnabled; // cast to boolean
     const slackChannels = req.body.slackChannels || null;
-    const isSyncRevisionToHackmd = !!req.body.isSyncRevisionToHackmd; // cast to boolean
     const pageTags = req.body.pageTags || undefined;
 
     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));
     }
 
-    const options = { isSyncRevisionToHackmd, overwriteScopesOfDescendants };
+    const options = { overwriteScopesOfDescendants };
     if (grant != null) {
       options.grant = grant;
       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,
     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: {
   //   ns:      'crowi',
   //   key:     'security:passport-google:clientId',

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

@@ -20,11 +20,11 @@ import {
   PageDeleteConfigValue, IPageDeleteConfigValueToProcessValidation,
 } from '~/interfaces/page-delete-config';
 import {
-  IPageOperationProcessInfo, IPageOperationProcessData, PageActionStage, PageActionType,
+  type IPageOperationProcessInfo, type IPageOperationProcessData, PageActionStage, PageActionType,
 } from '~/interfaces/page-operation';
-import { PageMigrationErrorData, SocketEventName, UpdateDescCountRawData } from '~/interfaces/websocket';
+import { SocketEventName, type PageMigrationErrorData, type UpdateDescCountRawData } from '~/interfaces/websocket';
 import {
-  CreateMethod, PageCreateOptions, PageModel, PageDocument, pushRevision, PageQueryBuilder,
+  type CreateMethod, type PageCreateOptions, type PageModel, type PageDocument, pushRevision, PageQueryBuilder,
 } from '~/server/models/page';
 import { createBatchStream } from '~/server/util/batch-stream';
 import loggerFactory from '~/utils/logger';
@@ -32,9 +32,9 @@ import { prepareDeleteConfigValuesForCalc } from '~/utils/page-delete-config';
 
 import { ObjectIdLike } from '../interfaces/mongoose-utils';
 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 Subscription from '../models/subscription';
 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> {
     const { grant, grantedGroup } = grantData;
 
-    const options = {
+    const options: IOptionsForUpdate = {
       grant,
       grantUserGroupId: grantedGroup,
-      isSyncRevisionToHackmd: false,
     };
 
     return this.updatePage(page, null, null, user, options);
@@ -4011,17 +4010,12 @@ class PageService {
     let savedPage = await newPageData.save();
 
     // Update body
-    const isSyncRevisionToHackmd = options.isSyncRevisionToHackmd;
     const isBodyPresent = body != null && previousBody != null;
     const shouldUpdateBody = isBodyPresent;
     if (shouldUpdateBody) {
       const newRevision = await Revision.prepareRevision(newPageData, body, previousBody, user);
       savedPage = await pushRevision(savedPage, newRevision, user);
       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 grantUserGroupId = options.grantUserGroupId || pageData.grantUserGroupId; // use the previous data if absence
-    const isSyncRevisionToHackmd = options.isSyncRevisionToHackmd;
 
     await this.validateAppliedScope(user, grant, grantUserGroupId);
     pageData.applyScope(user, grant, grantUserGroupId);
@@ -4089,10 +4082,6 @@ class PageService {
       const newRevision = await Revision.prepareRevision(pageData, body, previousBody, user);
       savedPage = await pushRevision(savedPage, newRevision, user);
       await savedPage.populateDataToShowRevision();
-
-      if (isSyncRevisionToHackmd) {
-        savedPage = await Page.syncRevisionToHackmd(savedPage);
-      }
     }
 
     // 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 S2sMessage from '../../models/vo/s2s-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 { S2sMessagingService } from '../s2s-messaging/base';
+import { S2sMessageHandlable } from '../s2s-messaging/handlable';
 
 const logger = loggerFactory('growi:service:system-events:SyncPageStatusService');
 
@@ -125,17 +124,6 @@ class SyncPageStatusService implements S2sMessageHandlable {
 
       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);
 };
 
-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> => {
   return useContextSWR<Nullable<any>, Error>('isSearchPage', initialData);
 };

+ 9 - 19
apps/app/src/stores/remote-latest-page.ts

@@ -1,36 +1,31 @@
 import { useMemo, useCallback } from 'react';
 
 import type { IUser } from '@growi/core';
-import { SWRResponse } from 'swr';
-
-
-import { useRevisionIdHackmdSynced, useHasDraftOnHackmd } from './hackmd';
-import { useStaticSWR } from './use-static-swr';
+import { useSWRStatic } from '@growi/core/dist/swr';
+import type { SWRResponse } from 'swr';
 
 
 export const useRemoteRevisionId = (initialData?: string): SWRResponse<string, Error> => {
-  return useStaticSWR<string, Error>('remoteRevisionId', initialData);
+  return useSWRStatic<string, Error>('remoteRevisionId', initialData);
 };
 
 export const useRemoteRevisionBody = (initialData?: string): SWRResponse<string, Error> => {
-  return useStaticSWR<string, Error>('remoteRevisionBody', initialData);
+  return useSWRStatic<string, Error>('remoteRevisionBody', initialData);
 };
 
 export const useRemoteRevisionLastUpdateUser = (initialData?: IUser): SWRResponse<IUser, Error> => {
-  return useStaticSWR<IUser, Error>('remoteRevisionLastUpdateUser', initialData);
+  return useSWRStatic<IUser, Error>('remoteRevisionLastUpdateUser', initialData);
 };
 
 export const useRemoteRevisionLastUpdatedAt = (initialData?: Date): SWRResponse<Date, Error> => {
-  return useStaticSWR<Date, Error>('remoteRevisionLastUpdatedAt', initialData);
+  return useSWRStatic<Date, Error>('remoteRevisionLastUpdatedAt', initialData);
 };
 
-type RemoteRevisionData = {
+export type RemoteRevisionData = {
   remoteRevisionId: string,
   remoteRevisionBody: string,
   remoteRevisionLastUpdateUser: IUser,
   remoteRevisionLastUpdatedAt: Date,
-  revisionIdHackmdSynced: string,
-  hasDraftOnHackmd: boolean,
 }
 
 
@@ -40,21 +35,16 @@ export const useSetRemoteLatestPageData = (): { setRemoteLatestPageData: (pageDa
   const { mutate: mutateRemoteRevisionBody } = useRemoteRevisionBody();
   const { mutate: mutateRemoteRevisionLastUpdateUser } = useRemoteRevisionLastUpdateUser();
   const { mutate: mutateRemoteRevisionLastUpdatedAt } = useRemoteRevisionLastUpdatedAt();
-  const { mutate: mutateRevisionIdHackmdSynced } = useRevisionIdHackmdSynced();
-  const { mutate: mutateHasDraftOnHackmd } = useHasDraftOnHackmd();
 
   const setRemoteLatestPageData = useCallback((remoteRevisionData: RemoteRevisionData) => {
     const {
-      remoteRevisionId, remoteRevisionBody, remoteRevisionLastUpdateUser, remoteRevisionLastUpdatedAt, revisionIdHackmdSynced, hasDraftOnHackmd,
+      remoteRevisionId, remoteRevisionBody, remoteRevisionLastUpdateUser, remoteRevisionLastUpdatedAt,
     } = remoteRevisionData;
     mutateRemoteRevisionId(remoteRevisionId);
     mutateRemoteRevisionBody(remoteRevisionBody);
     mutateRemoteRevisionLastUpdateUser(remoteRevisionLastUpdateUser);
     mutateRemoteRevisionLastUpdatedAt(remoteRevisionLastUpdatedAt);
-    mutateRevisionIdHackmdSynced(revisionIdHackmdSynced);
-    mutateHasDraftOnHackmd(hasDraftOnHackmd);
-  // eslint-disable-next-line max-len
-  }, [mutateHasDraftOnHackmd, mutateRemoteRevisionBody, mutateRemoteRevisionId, mutateRemoteRevisionLastUpdateUser, mutateRemoteRevisionLastUpdatedAt, mutateRevisionIdHackmdSynced]);
+  }, [mutateRemoteRevisionBody, mutateRemoteRevisionId, mutateRemoteRevisionLastUpdateUser, mutateRemoteRevisionLastUpdatedAt]);
 
   return useMemo(() => {
     return {

+ 0 - 10
apps/app/src/stores/ui.tsx

@@ -42,7 +42,6 @@ const logger = loggerFactory('growi:stores:ui');
 export const EditorMode = {
   View: 'view',
   Editor: 'editor',
-  HackMD: 'hackmd',
 } as const;
 export type EditorMode = typeof EditorMode[keyof typeof EditorMode];
 
@@ -86,9 +85,6 @@ const getClassNamesByEditorMode = (editorMode: EditorMode | undefined): string[]
     case EditorMode.Editor:
       classNames.push('editing', 'builtin-editor');
       break;
-    case EditorMode.HackMD:
-      classNames.push('editing', 'hackmd');
-      break;
   }
 
   return classNames;
@@ -97,7 +93,6 @@ const getClassNamesByEditorMode = (editorMode: EditorMode | undefined): string[]
 export const EditorModeHash = {
   View: '',
   Edit: '#edit',
-  HackMD: '#hackmd',
 } as const;
 export type EditorModeHash = typeof EditorModeHash[keyof typeof EditorModeHash];
 
@@ -113,9 +108,6 @@ const updateHashByEditorMode = (newEditorMode: EditorMode) => {
     case EditorMode.Editor:
       window.history.replaceState(null, '', `${pathname}${search}${EditorModeHash.Edit}`);
       break;
-    case EditorMode.HackMD:
-      window.history.replaceState(null, '', `${pathname}${search}${EditorModeHash.HackMD}`);
-      break;
   }
 };
 
@@ -129,8 +121,6 @@ export const determineEditorModeByHash = (): EditorMode => {
   switch (hash) {
     case EditorModeHash.Edit:
       return EditorMode.Editor;
-    case EditorModeHash.HackMD:
-      return EditorMode.HackMD;
     default:
       return EditorMode.View;
   }

+ 0 - 48
apps/app/src/styles/_editor.scss

@@ -40,24 +40,6 @@
     display: none !important;
   }
 
-  // hide when HackMD view
-  &.hackmd .d-hackmd-none {
-    display: none;
-  }
-
-  // show only either Edit button or HackMD button
-  &.hackmd .nav-tab-edit {
-    display: none;
-  }
-
-  &.hackmd .grw-nav-item-edit {
-    display: none;
-  }
-
-  &:not(.hackmd) .nav-tab-hackmd {
-    display: none;
-  }
-
 
   /*****************
    * Expand Editor
@@ -224,36 +206,6 @@
 
   // .builtin-editor .tab-pane#edit
 
-  &.hackmd {
-    .hackmd-preinit,
-    #iframe-hackmd-container > iframe {
-      border: none;
-    }
-
-    .hackmd-error {
-      top: 0;
-      background-color: rgba(bs.$gray-800, 0.8);
-    }
-
-    .hackmd-status-label {
-      font-size: 3em;
-    }
-
-    .hackmd-resume-button-container,
-    .hackmd-discard-button-container {
-      .btn-text {
-        display: inline-block;
-        min-width: 230px;
-      }
-    }
-
-    .btn-view-outdated-draft {
-      text-decoration: underline;
-      vertical-align: unset;
-    }
-  }
-
-
   /*****************
    *     Toastr
    *****************/

+ 14 - 0
apps/app/src/styles/molecules/_list-group-item.scss

@@ -0,0 +1,14 @@
+@use '@growi/core/scss/bootstrap/init' as bs;
+
+@include bs.color-mode(light) {
+  .list-group-item-action {
+    --bs-list-group-action-hover-bg: var(--grw-primary-200);
+    --bs-list-group-action-active-bg: var(--grw-primary-400);
+  }
+}
+@include bs.color-mode(dark) {
+  .list-group-item-action {
+    --bs-list-group-action-hover-bg: var(--grw-primary-800);
+    --bs-list-group-action-active-bg: var(--grw-primary-800);
+  }
+}

+ 8 - 7
apps/app/src/styles/organisms/_wiki.scss

@@ -16,9 +16,7 @@
 
   a {
     @extend .link-offset-2;
-    @extend .link-underline-opacity-25;
-    @extend .link-underline-opacity-50-hover;
-    text-decoration: underline;
+    text-decoration-line: underline;
   }
 
   // @extend .text-break;
@@ -297,10 +295,13 @@
 // == Colors
 .wiki {
   a {
-    color: rgba(
-      var(--grw-wiki-link-color-rgb, var(--bs-link-color-rgb)),
-      var(--bs-link-opacity, 1)
-    );
+    @extend .link-underline-opacity-25;
+    @extend .link-underline-opacity-100-hover;
+
+    $color-rgb: var(--grw-wiki-link-color-rgb, var(--bs-link-color-rgb));
+
+    color: rgba($color-rgb, var(--bs-link-opacity, 1));
+    text-decoration-color: rgba($color-rgb, var(--bs-link-underline-opacity, 1));
 
     &:hover {
       color: rgba(

+ 1 - 0
apps/app/src/styles/style-app.scss

@@ -10,6 +10,7 @@
 
 // molecules
 @import 'molecules/toastr';
+@import 'molecules/list-group-item';
 // @import 'molecules/slack-notification';
 // @import 'molecules/duplicated-paths-table.scss';
 

+ 0 - 1
apps/app/tsconfig.build.server.json

@@ -24,6 +24,5 @@
     "src/linter-checker",
     "src/stores",
     "src/styles",
-    "src/styles-hackmd"
   ]
 }

+ 13 - 9
packages/core/scss/bootstrap/theming/_variables.scss

@@ -5,12 +5,16 @@
 
 // Color system
 
-$gray-100: #d2d0ce !default;
-$gray-200: #c9c7c7 !default;
-$gray-300: #bab8b8 !default;
-$gray-400: #adaaaa !default;
-$gray-500: #9a9898 !default;
-$gray-600: #8a8886 !default;
-$gray-700: #585755 !default;
-$gray-800: #323130 !default;
-$gray-900: #171615 !default;
+$gray-100: #f8f9fa !default;
+$gray-200: #e9ecef !default;
+$gray-300: #dee2e6 !default;
+$gray-400: #ced4da !default;
+$gray-500: #adb5bd !default;
+$gray-600: #6c757d !default;
+$gray-700: #495057 !default;
+$gray-800: #343a40 !default;
+$gray-900: #212529 !default;
+
+// The contrast ratio to reach against white, to determine if color changes from "light" to "dark". Acceptable values for WCAG 2.0 are 3, 4.5 and 7.
+// See https://www.w3.org/TR/WCAG20/#visual-audio-contrast-contrast
+$min-contrast-ratio: 3 !default;

+ 28 - 28
packages/core/scss/bootstrap/theming/utils/_color-palette.scss

@@ -1,31 +1,31 @@
-@mixin generate-color-palette($color, $value, $shade-color-ratio: 20%, $tint-color-ratio: 20%, $prefix: 'grw-') {
-  $color-900: shade-color($value, $shade-color-ratio * 4);
-  $color-800: shade-color($value, $shade-color-ratio * 3);
-  $color-700: shade-color($value, $shade-color-ratio * 2);
-  $color-600: shade-color($value, $shade-color-ratio);
-  $color-500: $value;
-  $color-400: tint-color($value, $tint-color-ratio);
-  $color-300: tint-color($value, $tint-color-ratio * 2);
-  $color-200: tint-color($value, $tint-color-ratio * 3);
-  $color-100: tint-color($value, $tint-color-ratio * 4);
+@mixin generate-color-palette($color-id, $color-value, $shade-color: black, $tint-color: white, $shade-color-ratio: 20%, $tint-color-ratio: 20%, $prefix: 'grw-') {
+  $color-900: mix($shade-color, $color-value, $shade-color-ratio * 4);
+  $color-800: mix($shade-color, $color-value, $shade-color-ratio * 3);
+  $color-700: mix($shade-color, $color-value, $shade-color-ratio * 2);
+  $color-600: mix($shade-color, $color-value, $shade-color-ratio);
+  $color-500: $color-value;
+  $color-400: mix($tint-color, $color-value, $tint-color-ratio);
+  $color-300: mix($tint-color, $color-value, $tint-color-ratio * 2);
+  $color-200: mix($tint-color, $color-value, $tint-color-ratio * 3);
+  $color-100: mix($tint-color, $color-value, $tint-color-ratio * 4);
 
-  --#{$prefix}#{$color}-900: #{$color-900};
-  --#{$prefix}#{$color}-800: #{$color-800};
-  --#{$prefix}#{$color}-700: #{$color-700};
-  --#{$prefix}#{$color}-600: #{$color-600};
-  --#{$prefix}#{$color}-500: #{$color-500};
-  --#{$prefix}#{$color}-400: #{$color-400};
-  --#{$prefix}#{$color}-300: #{$color-300};
-  --#{$prefix}#{$color}-200: #{$color-200};
-  --#{$prefix}#{$color}-100: #{$color-100};
+  --#{$prefix}#{$color-id}-900: #{$color-900};
+  --#{$prefix}#{$color-id}-800: #{$color-800};
+  --#{$prefix}#{$color-id}-700: #{$color-700};
+  --#{$prefix}#{$color-id}-600: #{$color-600};
+  --#{$prefix}#{$color-id}-500: #{$color-500};
+  --#{$prefix}#{$color-id}-400: #{$color-400};
+  --#{$prefix}#{$color-id}-300: #{$color-300};
+  --#{$prefix}#{$color-id}-200: #{$color-200};
+  --#{$prefix}#{$color-id}-100: #{$color-100};
 
-  --#{$prefix}#{$color}-900-rgb: #{to-rgb($color-900)};
-  --#{$prefix}#{$color}-800-rgb: #{to-rgb($color-800)};
-  --#{$prefix}#{$color}-700-rgb: #{to-rgb($color-700)};
-  --#{$prefix}#{$color}-600-rgb: #{to-rgb($color-600)};
-  --#{$prefix}#{$color}-500-rgb: #{to-rgb($color-500)};
-  --#{$prefix}#{$color}-400-rgb: #{to-rgb($color-400)};
-  --#{$prefix}#{$color}-300-rgb: #{to-rgb($color-300)};
-  --#{$prefix}#{$color}-200-rgb: #{to-rgb($color-200)};
-  --#{$prefix}#{$color}-100-rgb: #{to-rgb($color-100)};
+  --#{$prefix}#{$color-id}-900-rgb: #{to-rgb($color-900)};
+  --#{$prefix}#{$color-id}-800-rgb: #{to-rgb($color-800)};
+  --#{$prefix}#{$color-id}-700-rgb: #{to-rgb($color-700)};
+  --#{$prefix}#{$color-id}-600-rgb: #{to-rgb($color-600)};
+  --#{$prefix}#{$color-id}-500-rgb: #{to-rgb($color-500)};
+  --#{$prefix}#{$color-id}-400-rgb: #{to-rgb($color-400)};
+  --#{$prefix}#{$color-id}-300-rgb: #{to-rgb($color-300)};
+  --#{$prefix}#{$color-id}-200-rgb: #{to-rgb($color-200)};
+  --#{$prefix}#{$color-id}-100-rgb: #{to-rgb($color-100)};
 }

+ 0 - 3
packages/core/src/interfaces/page.ts

@@ -25,9 +25,6 @@ export type IPage = {
   liker: Ref<IUser>[],
   commentCount: number
   slackChannels: string,
-  pageIdOnHackmd: string,
-  revisionHackmdSynced: Ref<IRevision>,
-  hasDraftOnHackmd: boolean,
   deleteUser: Ref<IUser>,
   deletedAt: Date,
   latestRevision?: Ref<IRevision>,

+ 0 - 12
packages/editor/src/components/CodeMirrorEditor/CodeMirrorEditor.tsx

@@ -2,9 +2,7 @@ import {
   forwardRef, useMemo, useRef, useEffect, useCallback,
 } from 'react';
 
-import { defaultKeymap } from '@codemirror/commands';
 import { indentUnit } from '@codemirror/language';
-import { keymap } from '@codemirror/view';
 import type { ReactCodeMirrorProps } from '@uiw/react-codemirror';
 import { useDropzone } from 'react-dropzone';
 
@@ -46,16 +44,6 @@ export const CodeMirrorEditor = (props: Props): JSX.Element => {
   }, [onChange]);
   const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(editorKey, containerRef.current, cmProps);
 
-  useEffect(() => {
-    const extension = keymap.of([
-      ...defaultKeymap,
-    ]);
-
-    const cleanupFunction = codeMirrorEditor?.appendExtensions?.(extension);
-    return cleanupFunction;
-
-  }, [codeMirrorEditor]);
-
   useEffect(() => {
     if (indentSize == null) {
       return;

+ 65 - 0
packages/editor/src/components/CodeMirrorEditorComment.tsx

@@ -0,0 +1,65 @@
+import { useEffect } from 'react';
+
+import type { Extension } from '@codemirror/state';
+import { keymap, scrollPastEnd } from '@codemirror/view';
+
+import { GlobalCodeMirrorEditorKey } from '../consts';
+import { useCodeMirrorEditorIsolated } from '../stores';
+
+import { CodeMirrorEditor } from '.';
+
+
+const additionalExtensions: Extension[] = [
+  scrollPastEnd(),
+];
+
+
+type Props = {
+  onChange?: (value: string) => void,
+  onComment?: () => void,
+}
+
+export const CodeMirrorEditorComment = (props: Props): JSX.Element => {
+  const {
+    onComment, onChange,
+  } = props;
+
+  const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.COMMENT);
+
+  // setup additional extensions
+  useEffect(() => {
+    return codeMirrorEditor?.appendExtensions?.(additionalExtensions);
+  }, [codeMirrorEditor]);
+
+  // set handler to comment with ctrl/cmd + Enter key
+  useEffect(() => {
+    if (onComment == null) {
+      return;
+    }
+
+    const keymapExtension = keymap.of([
+      {
+        key: 'Mod-Enter',
+        preventDefault: true,
+        run: () => {
+          const doc = codeMirrorEditor?.getDoc();
+          if (doc != null) {
+            onComment();
+          }
+          return true;
+        },
+      },
+    ]);
+
+    const cleanupFunction = codeMirrorEditor?.appendExtensions?.(keymapExtension);
+
+    return cleanupFunction;
+  }, [codeMirrorEditor, onComment]);
+
+  return (
+    <CodeMirrorEditor
+      editorKey={GlobalCodeMirrorEditorKey.COMMENT}
+      onChange={onChange}
+    />
+  );
+};

+ 1 - 0
packages/editor/src/consts/global-code-mirror-editor-key.ts

@@ -1,4 +1,5 @@
 export const GlobalCodeMirrorEditorKey = {
   MAIN: 'main',
+  COMMENT: 'comment',
 } as const;
 export type GlobalCodeMirrorEditorKey = typeof GlobalCodeMirrorEditorKey[keyof typeof GlobalCodeMirrorEditorKey]

+ 3 - 2
packages/editor/src/services/codemirror-editor/use-codemirror-editor/use-codemirror-editor.ts

@@ -1,9 +1,9 @@
 import { useMemo } from 'react';
 
-import { indentWithTab } from '@codemirror/commands';
+import { indentWithTab, defaultKeymap } from '@codemirror/commands';
 import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
 import { languages } from '@codemirror/language-data';
-import { EditorState, type Extension } from '@codemirror/state';
+import { EditorState, Prec, type Extension } from '@codemirror/state';
 import { keymap, EditorView } from '@codemirror/view';
 import { useCodeMirror, type UseCodeMirror } from '@uiw/react-codemirror';
 import deepmerge from 'ts-deepmerge';
@@ -32,6 +32,7 @@ export type UseCodeMirrorEditor = {
 const defaultExtensions: Extension[] = [
   markdown({ base: markdownLanguage, codeLanguages: languages }),
   keymap.of([indentWithTab]),
+  Prec.lowest(keymap.of(defaultKeymap)),
 ];
 
 export const useCodeMirrorEditor = (props?: UseCodeMirror): UseCodeMirrorEditor => {

+ 9 - 11
packages/preset-themes/src/styles/mono-blue.scss

@@ -3,14 +3,13 @@
   @import '@growi/core/scss/bootstrap/theming/variables';
   @import '@growi/core/scss/bootstrap/theming/utils/color-palette';
 
-  $primary: #409cb9;
-  $secondary: $gray-600;
+  $primary: #439cb9;
   $highlight: #93e3ea;
 
-  @include generate-color-palette('primary', $primary);
-  @include generate-color-palette('highlight', $highlight);
+  @include generate-color-palette('primary', $primary, #021529, white);
+  @include generate-color-palette('highlight', $highlight, black, #edffff);
 
-  $body-color:                $gray-900;
+  $body-color:                $gray-700;
   $body-bg:                   white;
 
   $body-secondary-color:      rgba($body-color, .75);
@@ -42,13 +41,12 @@
   @import '@growi/core/scss/bootstrap/theming/utils/color-palette';
 
   $primary: #439cb9;
-  $secondary: $gray-500;
-  $highlight: #64a9ed;
+  $highlight: #166cc0;
 
-  @include generate-color-palette('primary', $primary);
-  @include generate-color-palette('highlight', $highlight);
+  @include generate-color-palette('primary', $primary, #021529, white);
+  @include generate-color-palette('highlight', $highlight, black, white);
 
-  $body-color-dark:                   $gray-200;
+  $body-color-dark:                   $gray-400;
   $body-bg-dark:                      #16202c;
 
   $body-secondary-color-dark:         rgba($body-color-dark, .75);
@@ -59,7 +57,7 @@
 
   $border-color-dark:                 $gray-700;
 
-  $link-color-dark:                   $gray-300;
+  $link-color-dark:                   $gray-400;
 
   @import 'bootstrap/scss/variables';
   @import 'bootstrap/scss/variables-dark';

+ 0 - 7
yarn.lock

@@ -2921,9 +2921,6 @@
     react "^18.2.0"
     react-dom "^18.2.0"
 
-"@growi/hackmd@link:packages/hackmd":
-  version "7.0.0-RC.0"
-
 "@growi/pluginkit@link:packages/pluginkit":
   version "0.1.0"
   dependencies:
@@ -13846,10 +13843,6 @@ pend@~1.2.0:
   resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
   integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA=
 
-penpal@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/penpal/-/penpal-4.0.0.tgz#1cba7a64600c1e601f91dac393c21843c977bdec"
-
 performance-now@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"