Răsfoiți Sursa

Merge branch 'master' into fix/_reboot-bootstrap-colors.scss

ayaka0417 3 ani în urmă
părinte
comite
1b95434352
100 a modificat fișierele cu 569 adăugiri și 330 ștergeri
  1. 7 4
      .github/workflows/ci-app.yml
  2. 0 0
      packages/app/_obsolete/src/client/legacy/crowi.js
  3. 3 2
      packages/app/docker/Dockerfile
  4. 2 2
      packages/app/package.json
  5. 35 30
      packages/app/public/static/locales/en_US/admin.json
  6. 0 1
      packages/app/public/static/locales/en_US/translation.json
  7. 27 20
      packages/app/public/static/locales/ja_JP/admin.json
  8. 6 14
      packages/app/public/static/locales/zh_CN/admin.json
  9. 0 1
      packages/app/public/static/locales/zh_CN/translation.json
  10. 1 1
      packages/app/src/components/Admin/App/AppSettingsPageContents.tsx
  11. 2 5
      packages/app/src/components/Admin/PluginsExtension/PluginInstallerForm.tsx
  12. 1 4
      packages/app/src/components/BookmarkButtons.tsx
  13. 2 2
      packages/app/src/components/InAppNotification/InAppNotificationList.tsx
  14. 3 6
      packages/app/src/components/InAppNotification/PageNotification/PageModelNotification.tsx
  15. 1 4
      packages/app/src/components/LikeButtons.tsx
  16. 4 6
      packages/app/src/components/Navbar/AppearanceModeDropdown.tsx
  17. 0 5
      packages/app/src/components/Navbar/PageEditorModeManager.jsx
  18. 3 1
      packages/app/src/components/NotAvailableForGuest.jsx
  19. 1 1
      packages/app/src/components/Page.tsx
  20. 36 6
      packages/app/src/components/Page/DisplaySwitcher.tsx
  21. 14 15
      packages/app/src/components/Page/RenderTagLabels.tsx
  22. 2 1
      packages/app/src/components/PageAlert/TrashPageAlert.tsx
  23. 1 0
      packages/app/src/components/PageDeleteModal.tsx
  24. 31 10
      packages/app/src/components/PageEditor.tsx
  25. 98 45
      packages/app/src/components/PageEditor/ConflictDiffModal.tsx
  26. 1 1
      packages/app/src/components/PageEditor/DrawioCommunicationHelper.ts
  27. 6 4
      packages/app/src/components/PageEditorByHackmd.tsx
  28. 9 2
      packages/app/src/components/PageEditorByHackmd/HackmdEditor.jsx
  29. 6 6
      packages/app/src/components/PageStatusAlert.tsx
  30. 2 2
      packages/app/src/components/PutbackPageModal.jsx
  31. 1 1
      packages/app/src/components/ReactMarkdownComponents/DrawioViewerWithEditButton.tsx
  32. 1 1
      packages/app/src/components/Script/DrawioViewerScript.tsx
  33. 11 4
      packages/app/src/components/Sidebar/PageTree/Item.tsx
  34. 1 4
      packages/app/src/components/SubscribeButton.tsx
  35. 3 0
      packages/app/src/interfaces/in-app-notification.ts
  36. 1 0
      packages/app/src/interfaces/plugin.ts
  37. 3 0
      packages/app/src/interfaces/websocket.ts
  38. 2 2
      packages/app/src/models/serializers/in-app-notification-snapshot/page.ts
  39. 6 0
      packages/app/src/pages/[[...path]].page.tsx
  40. 2 2
      packages/app/src/pages/_document.page.tsx
  41. 1 1
      packages/app/src/server/crowi/express-init.js
  42. 1 1
      packages/app/src/server/routes/apiv3/index.js
  43. 1 2
      packages/app/src/server/routes/apiv3/plugins.ts
  44. 12 12
      packages/app/src/server/routes/login.js
  45. 66 38
      packages/app/src/server/service/plugin.ts
  46. 3 3
      packages/app/src/services/renderer/renderer.tsx
  47. 21 2
      packages/app/src/stores/in-app-notification.ts
  48. 27 0
      packages/app/src/stores/modal.tsx
  49. 45 3
      packages/app/src/stores/remote-latest-page.ts
  50. 6 0
      packages/app/src/styles/bootstrap/_variables.scss
  51. 2 2
      packages/app/src/styles/theme/_apply-colors.scss
  52. 1 1
      packages/app/src/styles/theme/_reboot-bootstrap-border-colors.scss
  53. 4 4
      packages/app/src/styles/theme/_reboot-toastr-colors.scss
  54. 17 5
      packages/app/test/cypress/integration/20-basic-features/20-basic-features--use-tools.spec.ts
  55. 2 0
      packages/app/test/cypress/integration/40-admin/40-admin--access-to-admin-page.spec.ts
  56. 0 1
      packages/hackmd/src/hackmd-styles.js
  57. 1 1
      packages/hackmd/src/index.js
  58. 6 0
      packages/preset-themes/src/styles/bootstrap/_variables.scss
  59. 4 4
      packages/preset-themes/src/styles/default.scss
  60. 0 8
      packages/preset-themes/src/styles/theme/_apply-colors.scss
  61. 0 4
      packages/preset-themes/src/styles/theme/_reboot-bootstrap-border-colors.scss
  62. 0 15
      packages/preset-themes/src/styles/theme/_reboot-toastr-colors.scss
  63. 0 0
      packages/remark-drawio/.eslintignore
  64. 0 0
      packages/remark-drawio/.eslintrc.js
  65. 0 0
      packages/remark-drawio/.gitignore
  66. 1 1
      packages/remark-drawio/README.md
  67. 1 1
      packages/remark-drawio/package.json
  68. 0 0
      packages/remark-drawio/src/components/DrawioViewer.module.scss
  69. 0 0
      packages/remark-drawio/src/components/DrawioViewer.tsx
  70. 1 1
      packages/remark-drawio/src/index.ts
  71. 0 0
      packages/remark-drawio/src/interfaces/graph-viewer.ts
  72. 0 0
      packages/remark-drawio/src/services/renderer/remark-drawio.ts
  73. 0 0
      packages/remark-drawio/src/utils/embed.ts
  74. 0 0
      packages/remark-drawio/src/utils/global.ts
  75. 0 0
      packages/remark-drawio/tsconfig.base.json
  76. 0 0
      packages/remark-drawio/tsconfig.build.json
  77. 0 0
      packages/remark-drawio/tsconfig.json
  78. 0 0
      packages/remark-growi-directive/.eslintignore
  79. 0 0
      packages/remark-growi-directive/.eslintrc.cjs
  80. 0 0
      packages/remark-growi-directive/.gitignore
  81. 1 1
      packages/remark-growi-directive/package.json
  82. 0 0
      packages/remark-growi-directive/readme.md
  83. 6 0
      packages/remark-growi-directive/src/index.js
  84. 0 0
      packages/remark-growi-directive/src/mdast-util-growi-directive/complex-types.d.ts
  85. 0 0
      packages/remark-growi-directive/src/mdast-util-growi-directive/consts.js
  86. 0 0
      packages/remark-growi-directive/src/mdast-util-growi-directive/index.js
  87. 0 0
      packages/remark-growi-directive/src/mdast-util-growi-directive/readme.md
  88. 0 0
      packages/remark-growi-directive/src/micromark-extension-growi-directive/index.js
  89. 0 0
      packages/remark-growi-directive/src/micromark-extension-growi-directive/lib/directive-leaf.js
  90. 0 0
      packages/remark-growi-directive/src/micromark-extension-growi-directive/lib/directive-text.js
  91. 0 0
      packages/remark-growi-directive/src/micromark-extension-growi-directive/lib/factory-attributes.js
  92. 0 0
      packages/remark-growi-directive/src/micromark-extension-growi-directive/lib/factory-label.js
  93. 0 0
      packages/remark-growi-directive/src/micromark-extension-growi-directive/lib/factory-name.js
  94. 1 1
      packages/remark-growi-directive/src/micromark-extension-growi-directive/lib/html.js
  95. 0 0
      packages/remark-growi-directive/src/micromark-extension-growi-directive/lib/syntax.js
  96. 0 0
      packages/remark-growi-directive/src/micromark-extension-growi-directive/readme.md
  97. 0 0
      packages/remark-growi-directive/src/micromark-factory-attributes-devider/index.d.ts
  98. 0 0
      packages/remark-growi-directive/src/micromark-factory-attributes-devider/index.js
  99. 0 0
      packages/remark-growi-directive/src/micromark-factory-attributes-devider/readme.md
  100. 3 3
      packages/remark-growi-directive/src/remark-growi-directive.js

+ 7 - 4
.github/workflows/ci-app.yml

@@ -52,12 +52,15 @@ jobs:
         run: |
           npx lerna bootstrap -- --frozen-lockfile
 
-      - name: lerna run lint for plugins
+      - name: lerna run lint for dependent packages
         run: |
-          yarn lerna run lint --scope @growi/remark-*
+          yarn lerna run lint --scope @growi/codemirror-textlint --scope @growi/core --scope @growi/hackmd --scope @growi/preset-themes --scope @growi/remark-* --scope @growi/slack --scope @growi/ui
+      - name: build dependent packages
+        run: |
+          yarn lerna run build --scope @growi/preset-themes
       - name: lerna run lint for app
         run: |
-          yarn lerna run lint --scope @growi/app --scope @growi/codemirror-textlint --scope @growi/core --scope @growi/slack --scope @growi/ui
+          yarn lerna run lint --scope @growi/app
 
       - name: Slack Notification
         uses: weseek/ghaction-slack-notification@master
@@ -123,7 +126,7 @@ jobs:
           name: Coverage Report
           path: |
             packages/app/coverage
-            packages/remark-growi-plugin/coverage
+            packages/remark-growi-directive/coverage
 
       - name: Slack Notification
         uses: weseek/ghaction-slack-notification@master

+ 0 - 0
packages/app/src/client/legacy/crowi.js → packages/app/_obsolete/src/client/legacy/crowi.js


+ 3 - 2
packages/app/docker/Dockerfile

@@ -107,10 +107,11 @@ COPY packages/core packages/core
 COPY packages/codemirror-textlint packages/codemirror-textlint
 COPY packages/slack packages/slack
 COPY packages/ui packages/ui
-COPY packages/remark-drawio-plugin packages/remark-drawio-plugin
-COPY packages/remark-growi-plugin packages/remark-growi-plugin
+COPY packages/remark-drawio packages/remark-drawio
+COPY packages/remark-growi-directive packages/remark-growi-directive
 COPY packages/remark-lsx packages/remark-lsx
 COPY packages/hackmd packages/hackmd
+COPY packages/preset-themes packages/preset-themes
 
 # build
 RUN yarn lerna run build

+ 2 - 2
packages/app/package.json

@@ -68,8 +68,8 @@
     "@growi/core": "^6.0.0-RC.9",
     "@growi/hackmd": "^6.0.0-RC.9",
     "@growi/preset-themes": "^6.0.0-RC.9",
-    "@growi/remark-drawio-plugin": "^6.0.0-RC.9",
-    "@growi/remark-growi-plugin": "^6.0.0-RC.9",
+    "@growi/remark-drawio": "^6.0.0-RC.9",
+    "@growi/remark-growi-directive": "^6.0.0-RC.9",
     "@growi/remark-lsx": "^6.0.0-RC.9",
     "@growi/slack": "^6.0.0-RC.9",
     "@promster/express": "^7.0.2",

+ 35 - 30
packages/app/public/static/locales/en_US/admin.json

@@ -2,11 +2,13 @@
   "meta": {
     "display_name": "English"
   },
-  "wiki_management_home_page": "Wiki Management Home Page",
   "last_login": "Last login",
-  "anyone_with_the_link": "anyone with the link",
-  "only_me": "only me",
-  "only_inside_the_group": "only inside the group",
+  "wiki_management_home_page": "Wiki Management Home Page",
+  "public": "Public",
+  "anyone_with_the_link": "Anyone with the link",
+  "specified_users": "Specified users",
+  "only_me": "Only me",
+  "only_inside_the_group": "Only inside the group",
   "security_settings": {
     "security_settings": "Security Settings",
     "scope_of_page_disclosure": "Scope of page disclosure",
@@ -203,7 +205,7 @@
         "username_detail": "Specification of mappings for <code>username</code> when creating new users",
         "name_detail": "Specification of mappings for <code>name</code> when creating new users",
         "mapping_detail": "Specification of mappings for %s when creating new users",
-        "register_1": "Contant to OIDC IdP Administrator",
+        "register_1": "Contact to OIDC IdP Administrator",
         "register_2": "Register your OIDC App with \"Authorization callback URL\" as <code>%s</code>",
         "register_3": "Copy and paste your ClientID and Client Secret above",
         "updated_oidc": "Succeeded to update OpenID Connect",
@@ -279,6 +281,29 @@
     "toggle_notification": "Updated setting of {{path}}",
     "not_found_global_notification_triggerid": "Not found the global notification id"
   },
+  "full_text_search_management": {
+    "full_text_search_management": "Full Text Search Management",
+    "elasticsearch_management": "Elasticsearch management",
+    "connection_status": "Connection status",
+    "connection_status_label_unconfigured": "UNCONFIGURED",
+    "connection_status_label_connected": "CONNECTED",
+    "connection_status_label_disconnected": "DISCONNECTED",
+    "connection_status_label_erroroccured": "ERROR OCCURED ON SEARCH SERVICE",
+    "indices_status": "Indices Status",
+    "indices_status_label_normalized": "NORMALIZED",
+    "indices_status_label_unnormalized": "REBUILDING or BROKEN",
+    "indices_summary": "Indices summary",
+    "reconnect": "Reconnect",
+    "reconnect_button": "Try to reconnect",
+    "reconnect_description": "Click the button to try to reconnect to Elasticsearch.",
+    "normalize": "Normalize",
+    "normalize_button": "Normalize indices",
+    "normalize_description": "Click the button to repair broken indices.",
+    "rebuild": "Rebuild",
+    "rebuild_button": "Rebuild index",
+    "rebuild_description_1": "Click the button to rebuild index and add all page datas.",
+    "rebuild_description_2": "This may take a while."
+  },
   "mailer_setup_required":"<a href='/admin/app'>Email settings</a> are required to send.",
   "admin_top": {
     "management_wiki": "Management Wiki",
@@ -497,6 +522,11 @@
   },
   "importer_management": {
     "import_data": "Import Data",
+    "article": "Article",
+    "category": "Category",
+    "tag": "Tag",
+    "page": "Page",
+    "page_path": "Page Path",
     "beta_warning": "This function is Beta.",
     "import_from": "Import from {{from}}",
     "import_growi_archive": "Import GROWI archive",
@@ -635,7 +665,6 @@
     "integration_procedure": "Integration Procedure",
     "custom_bot_without_proxy_settings": "Custom Bot without proxy Settings",
     "integration_failed":"Integration failed",
-    "official_bot_settings": "Official bot Settings",
     "reset": "Reset",
     "reset_all_settings": "Reset all settings",
     "delete_slackbot_settings": "Delete Slack Bot settings",
@@ -708,7 +737,6 @@
   "slack_integration_legacy": {
     "slack_integration_legacy": "Legacy Slack Integration",
     "alert_disabled": "This 'Slack Legacy Intenfation' has been currently disabled since <a href='/admin/slack-integration'>New settings</a> are enabled",
-    "alert_Pd": "This 'Legacy Slack Integration' is currently disabled because <a href='/admin/slack-integration'>the new settings</a> is enabled.",
     "alert_deplicated": "This 'Legacy Slack Integration' is outdated and will be discontinued in the future. Use <a href='/admin/slack-integration'>the new settings</a> instead. "
   },
   "user_management": {
@@ -818,29 +846,6 @@
       "force_update_parents_description": "Enable this option to force the addition of missing users to the ancestor groups if they exist after changing a parent group."
     }
   },
-  "full_text_search_management": {
-    "full_text_search_management": "Full Text Search Management",
-    "elasticsearch_management": "Elasticsearch management",
-    "connection_status": "Connection status",
-    "connection_status_label_unconfigured": "UNCONFIGURED",
-    "connection_status_label_connected": "CONNECTED",
-    "connection_status_label_disconnected": "DISCONNECTED",
-    "connection_status_label_erroroccured": "ERROR OCCURED ON SEARCH SERVICE",
-    "indices_status": "Indices Status",
-    "indices_status_label_normalized": "NORMALIZED",
-    "indices_status_label_unnormalized": "REBUILDING or BROKEN",
-    "indices_summary": "Indices summary",
-    "reconnect": "Reconnect",
-    "reconnect_button": "Try to reconnect",
-    "reconnect_description": "Click the button to try to reconnect to Elasticsearch.",
-    "normalize": "Normalize",
-    "normalize_button": "Normalize indices",
-    "normalize_description": "Click the button to repair broken indices.",
-    "rebuild": "Rebuild",
-    "rebuild_button": "Rebuild index",
-    "rebuild_description_1": "Click the button to rebuild index and add all page datas.",
-    "rebuild_description_2": "This may take a while."
-  },
   "audit_log_management": {
     "audit_log": "Audit Log",
     "audit_log_settings": "Audit Log Settings",

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

@@ -35,7 +35,6 @@
   "add": "Add",
   "Undo": "Undo",
   "Article": "Article",
-  "Page": "Page",
   "Page Path": "Page path",
   "Category": "Category",
   "User": "User",

+ 27 - 20
packages/app/public/static/locales/ja_JP/admin.json

@@ -64,6 +64,9 @@
     "xss_prevent_setting": "XSS(Cross Site Scripting)対策設定",
     "xss_prevent_setting_link": "マークダウン設定ページに移動",
     "callback_URL": "コールバックURL",
+    "providerName": "プロバイダ名",
+    "issuerHost": "発行ホスト",
+    "scope": "範囲",
     "desc_of_callback_URL": "{{AuthName}} プロバイダ側の設定で利用してください。",
     "authorization_endpoint": "認可エンドポイント",
     "token_endpoint": "トークンエンドポイント",
@@ -210,13 +213,17 @@
         "username_detail": "新規ユーザーのアカウント名(<code>username</code>)に関連付ける属性",
         "name_detail": "新規ユーザー名(<code>name</code>)に関連付ける属性",
         "mapping_detail": "新規ユーザーの{{target}}に関連付ける属性",
+        "register_1": "OIDC IdP Administrator へ接続します。",
+        "register_2": "OIDCアプリの認証コールバックURLを<code>%s</code>として登録します。",
+        "register_3": "上記のClientIDとClient Secretをコピー&ペーストしてください。",
         "updated_oidc": "OpenID Connect を更新しました",
         "Use discovered URL if empty": "データベース側の値が空の場合、\"Issuer Host\"から検出した値を利用します。"
       },
       "how_to": {
         "google": "Google OAuth の設定方法",
         "github": "GitHub OAuth の設定方法",
-        "twitter": "Twitter OAuth の設定方法"
+        "twitter": "Twitter OAuth の設定方法",
+        "oidc": "OIDC の設定方法"
       }
     },
     "form_item_name": {
@@ -521,25 +528,6 @@
     "upload_new_logo": "新しいロゴをアップロードする",
     "delete_logo": "ロゴを削除"
   },
-  "export_management": {
-    "export_archive_data": "データアーカイブ",
-    "exporting_collection_list": "エクスポート中のコレクション",
-    "exported_data_list": "エクスポートされたアーカイブリスト",
-    "export_collections": "コレクションのエクスポート",
-    "check_all": "全てにチェックを付ける",
-    "uncheck_all": "全てからチェックを外す",
-    "desc_password_seed": "<p>ユーザーデータをバックアップ/リストアする場合、現在の <code>PASSWORD_SEED</code> を新しい GROWI システムにセットすることを忘れないでください。さもなくば、ユーザーがパスワードでログインできなくなります。<br><br><strong>ヒント:</strong><br>現在の <code>PASSWORD_SEED</code> は、エクスポートされる ZIP 中の <code>meta.json</code> に保存されます。</p>",
-    "create_new_archive_data": "アーカイブデータの新規作成",
-    "export": "エクスポート",
-    "cancel": "キャンセル",
-    "file": "ファイル名",
-    "growi_version": "GROWI バージョン",
-    "collections": "コレクション",
-    "exported_at": "エクスポートされた時間",
-    "export_menu": "エクスポートメニュー",
-    "download": "ダウンロード",
-    "delete": "削除"
-  },
   "importer_management": {
     "import_data": "データインポート",
     "article": "記事",
@@ -617,6 +605,25 @@
     "page_skip": "既に GROWI 側に同名のページが存在する場合、そのページはスキップされます",
     "Directory_hierarchy_tag": "ディレクトリ階層タグ"
   },
+  "export_management": {
+    "export_archive_data": "データアーカイブ",
+    "exporting_collection_list": "エクスポート中のコレクション",
+    "exported_data_list": "エクスポートされたアーカイブリスト",
+    "export_collections": "コレクションのエクスポート",
+    "check_all": "全てにチェックを付ける",
+    "uncheck_all": "全てからチェックを外す",
+    "desc_password_seed": "<p>ユーザーデータをバックアップ/リストアする場合、現在の <code>PASSWORD_SEED</code> を新しい GROWI システムにセットすることを忘れないでください。さもなくば、ユーザーがパスワードでログインできなくなります。<br><br><strong>ヒント:</strong><br>現在の <code>PASSWORD_SEED</code> は、エクスポートされる ZIP 中の <code>meta.json</code> に保存されます。</p>",
+    "create_new_archive_data": "アーカイブデータの新規作成",
+    "export": "エクスポート",
+    "cancel": "キャンセル",
+    "file": "ファイル名",
+    "growi_version": "GROWI バージョン",
+    "collections": "コレクション",
+    "exported_at": "エクスポートされた時間",
+    "export_menu": "エクスポートメニュー",
+    "download": "ダウンロード",
+    "delete": "削除"
+  },
   "external_notification": {
     "external_notification": "外部ツールへの通知",
     "enabled": "有効",

+ 6 - 14
packages/app/public/static/locales/zh_CN/admin.json

@@ -7,6 +7,7 @@
   "User": "用户",
   "Name": "姓名",
   "Created": "创建",
+  "Page": "页面",
   "Edit": "编辑",
   "Description": "描述",
   "last_login": "上次登录",
@@ -79,7 +80,7 @@
 		"client_secret": "客户机密",
 		"updated_general_security_setting": "更新安全设置成功",
 		"setup_not_completed_yet": "安装尚未完成",
-		"guest_mode": {
+    "guest_mode": {
 			"deny": "拒绝(仅限注册用户)",
 			"readonly": "接受(来宾可以只读)"
 		},
@@ -212,7 +213,7 @@
 				"username_detail": "Specification of mappings for <code>username</code> when creating new users",
 				"name_detail": "Specification of mappings for <code>name</code> when creating new users",
 				"mapping_detail": "Specification of mappings for %s when creating new users",
-				"register_1": "Contant to OIDC IdP Administrator",
+				"register_1": "Contact to OIDC IdP Administrator",
 				"register_2": "Register your OIDC App with \"Authorization callback URL\" as <code>%s</code>",
 				"register_3": "Copy and paste your ClientID and Client Secret above",
 				"updated_oidc": "Succeeded to update OpenID Connect",
@@ -238,6 +239,7 @@
 		}
   },
   "notification_settings": {
+    "notification_settings": "通知设置",
 		"slack_incoming_configuration": "Slack Incoming Webhooks configuration",
 		"prioritize_webhook": "Prioritize incoming webhook than Slack App",
 		"prioritize_webhook_desc": "Check this option and GROWI use Incoming Webhooks even if Slack App settings are enabled.",
@@ -399,9 +401,10 @@
     "aws_label": "AWS(S3)",
     "local_label": "Local",
     "gridfs_label": "MongoDB(GridFS)",
+    "fixed_by_env_var": "这是由env var 修复的 <code>{{key}}={{value}}</code>.",
+    "file_upload": "This is for uploading file settings. If you complete file upload settings, file upload function, profile picture function etc will be enabled.",
     "ses_settings": "SES设置",
     "test_connection": "测试邮件服务器连接",
-    "": "如果您没有SMTP设置,电子邮件将通过SES发送。您需要从电子邮件地址和生产设置进行验证。",
     "change_setting": "注意:如果你更改此设置未完成,您将无法访问迄今为止上传的文件。",
     "region": "Region",
     "bucket_name": "Bucket name",
@@ -474,17 +477,6 @@
       "expanded": "内容宽度100% "
     },
     "theme": "主体",
-    "behavior": "行为",
-    "behavior_desc": {
-      "growi_text1": "<code>/page</code> and <code>/page/</code> 都显示同一页。",
-      "growi_text2": "<code>/nonexistent_page</code> 显示编辑表单",
-      "growi_text3": "如果使用GROWI增强布局,则所有页面都显示子页面列表",
-      "crowi_text1": "<code>/page</code> 显示页面",
-      "crowi_text2": "<code>/page/</code> 显示子页列表",
-      "crowi_text3": "如果portal应用于<code>/page/</code>,则会显示portal和子页面列表",
-      "crowi_text4": "<code>/nonexistent_page</code> 显示编辑表单<",
-      "crowi_text5": "<code>/nonexistent_page/</code> 子页列表"
-    },
     "theme_desc": {
       "light_and_dark": "明暗模式",
       "unique": "只有一种模式"

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

@@ -117,7 +117,6 @@
   "GROWI.5.0_new_schema": "GROWI.5.0 new schema",
   "See_more_detail_on_new_schema": "更多详情请见<a href='https://docs.growi.org/en/admin-guide/upgrading/50x.html#about-the-new-v5-compatible-format' target='_blank'> {{title}}</a> <i class='icon-share-alt'></i> ",
 	"Markdown Settings": "Markdown设置",
-	"Notification Settings": "通知设置",
 	"external_account_management": "外部账户管理",
   "UserGroup": "用户组",
   "ChildUserGroup": "儿童用户组",

+ 1 - 1
packages/app/src/components/Admin/App/AppSettingsPageContents.tsx

@@ -73,7 +73,7 @@ const AppSettingsPageContents = (props: Props) => {
           && (
             <div className="row">
               <div className="col-lg-12">
-                <h2 className="admin-setting-header">{t('V5 Page Migration')}</h2>
+                <h2 className="admin-setting-header" data-testid="v5-page-migration">{t('V5 Page Migration')}</h2>
                 <V5PageMigration />
               </div>
             </div>

+ 2 - 5
packages/app/src/components/Admin/PluginsExtension/PluginInstallerForm.tsx

@@ -1,9 +1,7 @@
 import React, { useCallback } from 'react';
 
-import { useTranslation } from 'react-i18next';
-
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Post } from '~/client/util/apiv3-client';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 
 import AdminInstallButtonRow from '../Common/AdminUpdateButtonRow';
 // TODO: error notification (toast, loggerFactory)
@@ -30,12 +28,11 @@ export const PluginInstallerForm = (): JSX.Element => {
     };
 
     try {
-      await apiv3Post('/plugins-extension', { pluginInstallerForm });
+      await apiv3Post('/plugins', { pluginInstallerForm });
       toastSuccess('Plugin Install Successed!');
     }
     catch (err) {
       toastError(err);
-      // logger.error(err);
     }
   }, []);
 

+ 1 - 4
packages/app/src/components/BookmarkButtons.tsx

@@ -41,15 +41,12 @@ const BookmarkButtons: FC<Props> = (props: Props) => {
   };
 
   const getTooltipMessage = useCallback(() => {
-    if (isGuestUser) {
-      return 'Not available for guest';
-    }
 
     if (isBookmarked) {
       return 'tooltip.cancel_bookmark';
     }
     return 'tooltip.bookmark';
-  }, [isGuestUser, isBookmarked]);
+  }, [isBookmarked]);
 
   return (
     <div className={`btn-group btn-group-bookmark ${styles['btn-group-bookmark']}`} role="group" aria-label="Bookmark buttons">

+ 2 - 2
packages/app/src/components/InAppNotification/InAppNotificationList.tsx

@@ -2,7 +2,7 @@ import React, { FC } from 'react';
 
 import { HasObjectId } from '@growi/core';
 
-import { IInAppNotification, PaginateResult } from '~/interfaces/in-app-notification';
+import type { IInAppNotification, PaginateResult } from '~/interfaces/in-app-notification';
 
 import InAppNotificationElm from './InAppNotificationElm';
 
@@ -26,7 +26,7 @@ const InAppNotificationList: FC<Props> = (props: Props) => {
     );
   }
 
-  const notifications = inAppNotificationData.docs;
+  const notifications = inAppNotificationData.docs.filter((notification) => { return notification.parsedSnapshot != null });
 
   return (
     <>

+ 3 - 6
packages/app/src/components/InAppNotification/PageNotification/PageModelNotification.tsx

@@ -6,10 +6,9 @@ import { HasObjectId } from '@growi/core';
 import { PagePathLabel } from '@growi/ui';
 import { useRouter } from 'next/router';
 
-import { IInAppNotificationOpenable } from '~/client/interfaces/in-app-notification-openable';
-import { IInAppNotification } from '~/interfaces/in-app-notification';
+import type { IInAppNotificationOpenable } from '~/client/interfaces/in-app-notification-openable';
+import type { IInAppNotification } from '~/interfaces/in-app-notification';
 
-import { parseSnapshot } from '../../../models/serializers/in-app-notification-snapshot/page';
 import FormattedDistanceDate from '../../FormattedDistanceDate';
 
 interface Props {
@@ -27,8 +26,6 @@ const PageModelNotification: ForwardRefRenderFunction<IInAppNotificationOpenable
 
   const router = useRouter();
 
-  const snapshot = parseSnapshot(notification.snapshot);
-
   // publish open()
   useImperativeHandle(ref, () => ({
     open() {
@@ -45,7 +42,7 @@ const PageModelNotification: ForwardRefRenderFunction<IInAppNotificationOpenable
   return (
     <div className="p-2 overflow-hidden">
       <div className="text-truncate">
-        <b>{actionUsers}</b> {actionMsg} <PagePathLabel path={snapshot.path} />
+        <b>{actionUsers}</b> {actionMsg} <PagePathLabel path={notification.parsedSnapshot?.path ?? ''} />
       </div>
       <i className={`${actionIcon} mr-2`} />
       <FormattedDistanceDate

+ 1 - 4
packages/app/src/components/LikeButtons.tsx

@@ -34,15 +34,12 @@ const LikeButtons: FC<LikeButtonsProps> = (props: LikeButtonsProps) => {
   } = props;
 
   const getTooltipMessage = useCallback(() => {
-    if (isGuestUser) {
-      return 'Not available for guest';
-    }
 
     if (isLiked) {
       return 'tooltip.cancel_like';
     }
     return 'tooltip.like';
-  }, [isGuestUser, isLiked]);
+  }, [isLiked]);
 
   return (
     <div className={`btn-group btn-group-like ${styles['btn-group-like']}`} role="group" aria-label="Like buttons">

+ 4 - 6
packages/app/src/components/Navbar/AppearanceModeDropdown.tsx

@@ -68,6 +68,8 @@ export const AppearanceModeDropdown:FC<AppearanceModeDropdownProps> = (props: Ap
     </>
   );
 
+  const dropdownDivider = <div className="dropdown-divider"></div>;
+
   const renderSidebarModeSwitch = useCallback((isEditMode: boolean) => {
     return (
       <>
@@ -111,14 +113,10 @@ export const AppearanceModeDropdown:FC<AppearanceModeDropdownProps> = (props: Ap
       <div className="dropdown-menu dropdown-menu-right">
 
         {/* sidebar mode */}
-        {renderSidebarModeSwitch(false)}
-
-        <div className="dropdown-divider"></div>
+        {[renderSidebarModeSwitch(false), dropdownDivider]}
 
         {/* side bar mode on editor */}
-        {isAuthenticated && renderSidebarModeSwitch(true)}
-
-        <div className="dropdown-divider"></div>
+        {isAuthenticated && [renderSidebarModeSwitch(true), dropdownDivider]}
 
         {/* color mode */}
         <h6 className="dropdown-header">{t('personal_dropdown.color_mode')}</h6>

+ 0 - 5
packages/app/src/components/Navbar/PageEditorModeManager.jsx

@@ -110,11 +110,6 @@ function PageEditorModeManager(props) {
           </>
         )}
       </div>
-      {isBtnDisabled && (
-        <UncontrolledTooltip placement="top" target="grw-page-editor-mode-manager" fade={false}>
-          {t('Not available for guest')}
-        </UncontrolledTooltip>
-      )}
     </>
   );
 

+ 3 - 1
packages/app/src/components/NotAvailableForGuest.jsx

@@ -1,5 +1,6 @@
 import React from 'react';
 
+import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import { UncontrolledTooltip } from 'reactstrap';
 
@@ -7,6 +8,7 @@ import { useIsGuestUser } from '~/stores/context';
 
 const NotAvailableForGuest = (props) => {
   const { children } = props;
+  const { t } = useTranslation();
 
   const { data: isGuestUser } = useIsGuestUser();
 
@@ -26,7 +28,7 @@ const NotAvailableForGuest = (props) => {
   return (
     <>
       { clonedChild }
-      <UncontrolledTooltip placement="top" target={id}>Not available for guest</UncontrolledTooltip>
+      <UncontrolledTooltip placement="top" target={id}>{t('Not available for guest')}</UncontrolledTooltip>
     </>
   );
 

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

@@ -5,7 +5,7 @@ import React, {
 
 import EventEmitter from 'events';
 
-import { DrawioEditByViewerProps } from '@growi/remark-drawio-plugin';
+import { DrawioEditByViewerProps } from '@growi/remark-drawio';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import { HtmlElementNode } from 'rehype-toc';

+ 36 - 6
packages/app/src/components/Page/DisplaySwitcher.tsx

@@ -10,9 +10,12 @@ import { SocketEventName } from '~/interfaces/websocket';
 import {
   useIsSharedUser, useIsEditable, useShareLinkId, useIsNotFound,
 } from '~/stores/context';
+import { useIsHackmdDraftUpdatingInRealtime } from '~/stores/hackmd';
 import { useDescendantsPageListModal } from '~/stores/modal';
 import { useCurrentPagePath, useSWRxCurrentPage } from '~/stores/page';
-import { useRemoteRevisionId, useRemoteRevisionLastUpdatUser } from '~/stores/remote-latest-page';
+import {
+  useSetRemoteLatestPageData,
+} from '~/stores/remote-latest-page';
 import { EditorMode, useEditorMode } from '~/stores/ui';
 import { useGlobalSocket } from '~/stores/websocket';
 
@@ -47,8 +50,9 @@ const PageView = React.memo((): JSX.Element => {
   const { data: isNotFound } = useIsNotFound();
   const { data: currentPage } = useSWRxCurrentPage(shareLinkId ?? undefined);
   const { open: openDescendantPageListModal } = useDescendantsPageListModal();
-  const { mutate: mutateRemoteRevisionId } = useRemoteRevisionId();
-  const { mutate: mutateRemoteRevisionLastUpdateUser } = useRemoteRevisionLastUpdatUser();
+  const { setRemoteLatestPageData } = useSetRemoteLatestPageData();
+
+  const { mutate: mutateIsHackmdDraftUpdatingInRealtime } = useIsHackmdDraftUpdatingInRealtime();
 
   const isTopPagePath = isTopPage(currentPagePath ?? '');
   const isUsersHomePagePath = isUsersHomePage(currentPagePath ?? '');
@@ -58,9 +62,23 @@ const PageView = React.memo((): JSX.Element => {
   const setLatestRemotePageData = useCallback((data) => {
     const { s2cMessagePageUpdated } = data;
 
-    mutateRemoteRevisionId(s2cMessagePageUpdated.revisionId);
-    mutateRemoteRevisionLastUpdateUser(s2cMessagePageUpdated.remoteLastUpdateUser);
-  }, [mutateRemoteRevisionId, mutateRemoteRevisionLastUpdateUser]);
+    const remoteData = {
+      remoteRevisionId: s2cMessagePageUpdated.revisionId,
+      remoteRevisionBody: s2cMessagePageUpdated.revisionBody,
+      remoteRevisionLastUpdateUser: s2cMessagePageUpdated.remoteLastUpdateUser,
+      remoteRevisionLastUpdatedAt: s2cMessagePageUpdated.revisionUpdateAt,
+      revisionIdHackmdSynced: s2cMessagePageUpdated.revisionIdHackmdSynced,
+      hasDraftOnHackmd: s2cMessagePageUpdated.hasDraftOnHackmd,
+    };
+    setRemoteLatestPageData(remoteData);
+  }, [setRemoteLatestPageData]);
+
+  const setIsHackmdDraftUpdatingInRealtime = useCallback((data) => {
+    const { s2cMessagePageUpdated } = data;
+    if (s2cMessagePageUpdated.pageId === currentPage?._id) {
+      mutateIsHackmdDraftUpdatingInRealtime(true);
+    }
+  }, [currentPage?._id, mutateIsHackmdDraftUpdatingInRealtime]);
 
   // listen socket for someone updating this page
   useEffect(() => {
@@ -75,6 +93,18 @@ const PageView = React.memo((): JSX.Element => {
 
   }, [setLatestRemotePageData, socket]);
 
+  // listen socket for hackmd saved
+  useEffect(() => {
+
+    if (socket == null) { return }
+
+    socket.on(SocketEventName.EditingWithHackmd, setIsHackmdDraftUpdatingInRealtime);
+
+    return () => {
+      socket.off(SocketEventName.EditingWithHackmd, setIsHackmdDraftUpdatingInRealtime);
+    };
+  }, [setIsHackmdDraftUpdatingInRealtime, socket]);
+
   return (
     <div className="d-flex flex-column flex-lg-row">
 

+ 14 - 15
packages/app/src/components/Page/RenderTagLabels.tsx

@@ -1,7 +1,8 @@
 import React from 'react';
 
 import { useTranslation } from 'next-i18next';
-import { UncontrolledTooltip } from 'reactstrap';
+
+import NotAvailableForGuest from '../NotAvailableForGuest';
 
 type RenderTagLabelsProps = {
   tags: string[],
@@ -31,21 +32,19 @@ const RenderTagLabels = React.memo((props: RenderTagLabelsProps) => {
           </a>
         );
       })}
-      <div id="edit-tags-btn-wrapper-for-tooltip">
-        <a
-          className={`btn btn-link btn-edit-tags text-muted p-0 d-flex align-items-center ${isTagsEmpty && 'no-tags'} ${isGuestUser && 'disabled'}`}
-          onClick={openEditorHandler}
-        >
-          { isTagsEmpty && <>{ t('Add tags for this page') }</>}
-          <i className={`icon-plus ${isTagsEmpty && 'ml-1'}`}/>
-        </a>
-      </div>
-      {isGuestUser && (
-        <UncontrolledTooltip placement="top" target="edit-tags-btn-wrapper-for-tooltip" fade={false}>
-          {t('Not available for guest')}
-        </UncontrolledTooltip>
-      )}
+      <NotAvailableForGuest>
+        <div id="edit-tags-btn-wrapper-for-tooltip">
+          <a
+            className={`btn btn-link btn-edit-tags text-muted p-0 d-flex align-items-center ${isTagsEmpty && 'no-tags'} ${isGuestUser && 'disabled'}`}
+            onClick={openEditorHandler}
+          >
+            { isTagsEmpty && <>{ t('Add tags for this page') }</>}
+            <i className={`icon-plus ${isTagsEmpty && 'ml-1'}`}/>
+          </a>
+        </div>
+      </NotAvailableForGuest>
     </>
+
   );
 
 });

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

@@ -73,6 +73,7 @@ export const TrashPageAlert = (): JSX.Element => {
           className="btn btn-info rounded-pill btn-sm ml-auto mr-2"
           onClick={openPutbackPageModalHandler}
           data-toggle="modal"
+          data-testid="put-back-button"
         >
           <i className="icon-action-undo" aria-hidden="true"></i> { t('Put Back') }
         </button>
@@ -94,7 +95,7 @@ export const TrashPageAlert = (): JSX.Element => {
 
   return (
     <>
-      <div className="alert alert-warning py-3 pl-4 d-flex flex-column flex-lg-row">
+      <div className="alert alert-warning py-3 pl-4 d-flex flex-column flex-lg-row" data-testid="trash-page-alert">
         <div className="flex-grow-1">
           This page is in the trash <i className="icon-trash" aria-hidden="true"></i>.
           <br />

+ 1 - 0
packages/app/src/components/PageDeleteModal.tsx

@@ -272,6 +272,7 @@ const PageDeleteModal: FC = () => {
           className={`btn btn-${deleteIconAndKey[deleteMode].color}`}
           disabled={!isDeletable}
           onClick={deleteButtonHandler}
+          data-testid="delete-page-button"
         >
           <i className={`mr-1 icon-${deleteIconAndKey[deleteMode].icon}`} aria-hidden="true"></i>
           { t(`modal_delete.delete_${deleteIconAndKey[deleteMode].translationKey}`) }

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

@@ -29,7 +29,8 @@ import {
   useIsConflict,
   useEditingMarkdown,
 } from '~/stores/editor';
-import { useCurrentPagePath, useSWRxCurrentPage } from '~/stores/page';
+import { useConflictDiffModal } from '~/stores/modal';
+import { useCurrentPagePath, useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
 import { usePreviewOptions } from '~/stores/renderer';
 import {
   EditorMode,
@@ -41,6 +42,7 @@ import loggerFactory from '~/utils/logger';
 
 
 // import { ConflictDiffModal } from './PageEditor/ConflictDiffModal';
+import { ConflictDiffModal } from './PageEditor/ConflictDiffModal';
 import Editor from './PageEditor/Editor';
 import Preview from './PageEditor/Preview';
 import scrollSyncHelper from './PageEditor/ScrollSyncHelper';
@@ -71,8 +73,9 @@ const PageEditor = React.memo((): JSX.Element => {
   const { data: currentPathname } = useCurrentPathname();
   const { data: currentPage, mutate: mutateCurrentPage } = useSWRxCurrentPage();
   const { data: grantData, mutate: mutateGrant } = useSelectedGrant();
-  const { data: pageTags } = usePageTagsForEditors(pageId);
-  const { data: editingMarkdown } = useEditingMarkdown();
+  const { data: pageTags, sync: syncTagsInfoForEditor } = usePageTagsForEditors(pageId);
+  const { mutate: mutateTagsInfo } = useSWRxTagsInfo(pageId);
+  const { data: editingMarkdown, mutate: mutateEditingMarkdown } = useEditingMarkdown();
   const { data: isEnabledAttachTitleHeader } = useIsEnabledAttachTitleHeader();
   const { data: templateBodyData } = useTemplateBodyData();
   const { data: isEditable } = useIsEditable();
@@ -84,6 +87,7 @@ const PageEditor = React.memo((): JSX.Element => {
   const { data: currentIndentSize, mutate: mutateCurrentIndentSize } = useCurrentIndentSize();
   const { data: isUploadableFile } = useIsUploadableFile();
   const { data: isUploadableImage } = useIsUploadableImage();
+  const { data: conflictDiffModalStatus, close: closeConflictDiffModal } = useConflictDiffModal();
 
   const { data: rendererOptions, mutate: mutateRendererOptions } = usePreviewOptions();
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
@@ -411,6 +415,23 @@ const PageEditor = React.memo((): JSX.Element => {
   }, []);
   const scrollEditorByPreviewScrollWithThrottle = useMemo(() => throttle(20, scrollEditorByPreviewScroll), [scrollEditorByPreviewScroll]);
 
+  const afterResolvedHandler = useCallback(async() => {
+    // get page data from db
+    const pageData = await mutateCurrentPage();
+
+    // update tag
+    await mutateTagsInfo(); // get from DB
+    syncTagsInfoForEditor(); // sync global state for client
+
+    // clear isConflict
+    mutateIsConflict(false);
+
+    // set resolved markdown in editing markdown
+    const markdown = pageData?.revision.body ?? '';
+    mutateEditingMarkdown(markdown);
+
+  }, [mutateCurrentPage, mutateEditingMarkdown, mutateIsConflict, mutateTagsInfo, syncTagsInfoForEditor]);
+
 
   // initialize
   useEffect(() => {
@@ -514,13 +535,13 @@ const PageEditor = React.memo((): JSX.Element => {
           onScroll={offset => scrollEditorByPreviewScrollWithThrottle(offset)}
         />
       </div>
-      {/* <ConflictDiffModal
-        isOpen={pageContainer.state.isConflictDiffModalOpen}
-        onClose={() => pageContainer.setState({ isConflictDiffModalOpen: false })}
-        pageContainer={pageContainer}
-        markdownOnEdit={markdown}
-        optionsToSave={optionsToSave}
-      /> */}
+      <ConflictDiffModal
+        isOpen={conflictDiffModalStatus?.isOpened}
+        onClose={() => closeConflictDiffModal()}
+        markdownOnEdit={markdownToPreview}
+        optionsToSave={undefined} // replace undefined
+        afterResolvedHandler={afterResolvedHandler}
+      />
     </div>
   );
 });

+ 98 - 45
packages/app/src/components/PageEditor/ConflictDiffModal.tsx

@@ -2,7 +2,6 @@ import React, {
   useState, useEffect, useRef, useMemo, useCallback,
 } from 'react';
 
-import type { IUser } from '@growi/core';
 import { UserPicture } from '@growi/ui';
 import CodeMirror from 'codemirror/lib/codemirror';
 import { format } from 'date-fns';
@@ -11,9 +10,14 @@ import {
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 
+import { useSaveOrUpdate } from '~/client/services/page-operation';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import { OptionsToSave } from '~/interfaces/page-operation';
-import { useCurrentUser } from '~/stores/context';
-import { useEditorMode } from '~/stores/ui';
+import { useCurrentPageId, useCurrentPathname, useCurrentUser } from '~/stores/context';
+import { useCurrentPagePath, useSWRxCurrentPage } from '~/stores/page';
+import {
+  useRemoteRevisionBody, useRemoteRevisionId, useRemoteRevisionLastUpdatedAt, useRemoteRevisionLastUpdateUser, useSetRemoteLatestPageData,
+} from '~/stores/remote-latest-page';
 
 import { IRevisionOnConflict } from '../../interfaces/revision';
 import ExpandOrContractButton from '../ExpandOrContractButton';
@@ -29,19 +33,29 @@ Object.keys(DMP).forEach((key) => { window[key] = DMP[key] });
 type ConflictDiffModalProps = {
   isOpen?: boolean;
   onClose?: (() => void);
-  // pageContainer: PageContainer;
   markdownOnEdit: string;
   optionsToSave: OptionsToSave | undefined;
+  afterResolvedHandler: () => void,
+};
+
+type ConflictDiffModalCoreProps = {
+  isOpen?: boolean;
+  onClose?: (() => void);
+  optionsToSave: OptionsToSave | undefined;
+  request: IRevisionOnConflictWithStringDate,
+  origin: IRevisionOnConflictWithStringDate,
+  latest: IRevisionOnConflictWithStringDate,
+  afterResolvedHandler: () => void,
 };
 
 type IRevisionOnConflictWithStringDate = Omit<IRevisionOnConflict, 'createdAt'> & {
   createdAt: string
 }
 
-const ConflictDiffModalCore = (props: ConflictDiffModalProps & { currentUser: IUser }): JSX.Element => {
-  const { currentUser, onClose } = props;
-
-  const { data: editorMode } = useEditorMode();
+const ConflictDiffModalCore = (props: ConflictDiffModalCoreProps): JSX.Element => {
+  const {
+    onClose, request, origin, latest, optionsToSave, afterResolvedHandler,
+  } = props;
 
   const { t } = useTranslation('');
   const [resolvedRevision, setResolvedRevision] = useState<string>('');
@@ -49,37 +63,15 @@ const ConflictDiffModalCore = (props: ConflictDiffModalProps & { currentUser: IU
   const [isModalExpanded, setIsModalExpanded] = useState<boolean>(false);
   const [codeMirrorRef, setCodeMirrorRef] = useState<HTMLDivElement | null>(null);
 
-  const uncontrolledRef = useRef<CodeMirror>(null);
+  const { data: remoteRevisionId } = useRemoteRevisionId();
+  const { setRemoteLatestPageData } = useSetRemoteLatestPageData();
+  const { data: pageId } = useCurrentPageId();
+  const { data: currentPagePath } = useCurrentPagePath();
+  const { data: currentPathname } = useCurrentPathname();
 
-  const currentTime: Date = new Date();
+  const saveOrUpdate = useSaveOrUpdate();
 
-  const request: IRevisionOnConflictWithStringDate = {
-    revisionId: '',
-    revisionBody: props.markdownOnEdit,
-    createdAt: format(currentTime, 'yyyy/MM/dd HH:mm:ss'),
-    user: currentUser,
-  };
-  const origin: IRevisionOnConflictWithStringDate = {
-    // revisionId: pageContainer.state.revisionId || '',
-    // revisionBody: pageContainer.state.markdown || '',
-    // createdAt: pageContainer.state.updatedAt || '',
-    // user: pageContainer.state.revisionAuthor,
-    revisionId:  '',
-    revisionBody: '',
-    createdAt: '',
-    user: {} as IUser,
-  };
-  const latest: IRevisionOnConflictWithStringDate = {
-    // revisionId: pageContainer.state.remoteRevisionId || '',
-    // revisionBody: pageContainer.state.remoteRevisionBody || '',
-    // createdAt: format(new Date(pageContainer.state.remoteRevisionUpdateAt || currentTime.toString()), 'yyyy/MM/dd HH:mm:ss'),
-    // user: pageContainer.state.lastUpdateUser,
-    revisionId: '',
-    revisionBody: '',
-    createdAt: format(new Date(''), 'yyyy/MM/dd HH:mm:ss'),
-    user: {} as IUser,
-
-  };
+  const uncontrolledRef = useRef<CodeMirror>(null);
 
   useEffect(() => {
     if (codeMirrorRef != null) {
@@ -105,21 +97,38 @@ const ConflictDiffModalCore = (props: ConflictDiffModalProps & { currentUser: IU
   }, [onClose]);
 
   const onResolveConflict = useCallback(async() => {
+    if (currentPathname == null) { return }
     // disable button after clicked
     setIsRevisionSelected(false);
 
     const codeMirrorVal = uncontrolledRef.current?.editor.doc.getValue();
 
     try {
-      // await pageContainer.resolveConflict(codeMirrorVal, editorMode, props.optionsToSave);
-      // close();
-      // pageContainer.showSuccessToastr();
+      const { page } = await saveOrUpdate(
+        codeMirrorVal,
+        { pageId, path: currentPagePath || currentPathname, revisionId: remoteRevisionId },
+        optionsToSave,
+      );
+      const remotePageData = {
+        remoteRevisionId: page.revision._id,
+        remoteRevisionBody: page.revision.body,
+        remoteRevisionLastUpdateUser: page.lastUpdateUser,
+        remoteRevisionLastUpdatedAt: page.updatedAt,
+        revisionIdHackmdSynced: page.revisionIdHackmdSynced,
+        hasDraftOnHackmd: page.hasDraftOnHackmd,
+      };
+      setRemoteLatestPageData(remotePageData);
+      afterResolvedHandler();
+
+      close();
+
+      toastSuccess('Saved successfully');
     }
     catch (error) {
-      // pageContainer.showErrorToastr(error);
+      toastError(`Error occured: ${error.message}`);
     }
 
-  }, []);
+  }, [afterResolvedHandler, close, currentPagePath, currentPathname, optionsToSave, pageId, remoteRevisionId, saveOrUpdate, setRemoteLatestPageData]);
 
   const resizeAndCloseButtons = useMemo(() => (
     <div className="d-flex flex-nowrap">
@@ -274,12 +283,56 @@ const ConflictDiffModalCore = (props: ConflictDiffModalProps & { currentUser: IU
 
 
 export const ConflictDiffModal = (props: ConflictDiffModalProps): JSX.Element => {
-  const { isOpen } = props;
+  const {
+    isOpen, onClose, optionsToSave, afterResolvedHandler,
+  } = props;
   const { data: currentUser } = useCurrentUser();
 
-  if (!isOpen || currentUser == null) {
+  // state for current page
+  const { data: currentPage } = useSWRxCurrentPage();
+
+  // state for latest page
+  const { data: remoteRevisionId } = useRemoteRevisionId();
+  const { data: remoteRevisionBody } = useRemoteRevisionBody();
+  const { data: remoteRevisionLastUpdateUser } = useRemoteRevisionLastUpdateUser();
+  const { data: remoteRevisionLastUpdatedAt } = useRemoteRevisionLastUpdatedAt();
+
+  const currentTime: Date = new Date();
+
+  const isRemotePageDataInappropriate = remoteRevisionId == null || remoteRevisionBody == null || remoteRevisionLastUpdateUser == null;
+
+  if (!isOpen || currentUser == null || currentPage == null || isRemotePageDataInappropriate) {
     return <></>;
   }
 
-  return <ConflictDiffModalCore {...props} currentUser={currentUser} />;
+  const request: IRevisionOnConflictWithStringDate = {
+    revisionId: '',
+    revisionBody: props.markdownOnEdit,
+    createdAt: format(currentTime, 'yyyy/MM/dd HH:mm:ss'),
+    user: currentUser,
+  };
+  const origin: IRevisionOnConflictWithStringDate = {
+    revisionId: currentPage?.revision._id,
+    revisionBody: currentPage?.revision.body,
+    createdAt: format(currentPage.updatedAt, 'yyyy/MM/dd HH:mm:ss'),
+    user: currentPage?.lastUpdateUser,
+  };
+  const latest: IRevisionOnConflictWithStringDate = {
+    revisionId: remoteRevisionId,
+    revisionBody: remoteRevisionBody,
+    createdAt: format(new Date(remoteRevisionLastUpdatedAt || currentTime.toString()), 'yyyy/MM/dd HH:mm:ss'),
+    user: remoteRevisionLastUpdateUser,
+  };
+
+  const propsForCore = {
+    isOpen,
+    onClose,
+    optionsToSave,
+    request,
+    origin,
+    latest,
+    afterResolvedHandler,
+  };
+
+  return <ConflictDiffModalCore {...propsForCore}/>;
 };

+ 1 - 1
packages/app/src/components/PageEditor/DrawioCommunicationHelper.ts

@@ -1,4 +1,4 @@
-import { extractCodeFromMxfile } from '@growi/remark-drawio-plugin';
+import { extractCodeFromMxfile } from '@growi/remark-drawio';
 
 import loggerFactory from '~/utils/logger';
 

+ 6 - 4
packages/app/src/components/PageEditorByHackmd.tsx

@@ -85,7 +85,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
   const { data: hasDraftOnHackmd, mutate: mutateHasDraftOnHackmd } = useHasDraftOnHackmd();
   const { data: revisionIdHackmdSynced, mutate: mutateRevisionIdHackmdSynced } = useRevisionIdHackmdSynced();
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
-  const { data: isHackmdDraftUpdatingInRealtime, mutate: mutateIsHackmdDraftUpdatingInRealtime } = useIsHackmdDraftUpdatingInRealtime(false);
+  const { data: isHackmdDraftUpdatingInRealtime, mutate: mutateIsHackmdDraftUpdatingInRealtime } = useIsHackmdDraftUpdatingInRealtime();
   const { data: remoteRevisionId, mutate: mutateRemoteRevisionId } = useRemoteRevisionId(revision?._id);
 
   const hackmdEditorRef = useRef<HackEditorRef>(null);
@@ -94,7 +94,8 @@ export const PageEditorByHackmd = (): JSX.Element => {
     if (editorMode !== EditorMode.HackMD) { return }
 
     try {
-      if (isSlackEnabled == null || currentPathname == null || slackChannels == null || grant == null || revision == null || hackmdEditorRef.current == null) {
+      if (isSlackEnabled == null || currentPathname == null || slackChannels == null || grant == null
+          || revision == null || hackmdEditorRef.current == null || revisionIdHackmdSynced == null) {
         throw new Error('Some materials to save are invalid');
       }
 
@@ -111,7 +112,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
 
       const markdown = await hackmdEditorRef.current.getValue();
 
-      const { page } = await saveOrUpdate(markdown, { pageId, path: currentPagePath || currentPathname, revisionId: revision?._id }, optionsToSave);
+      const { page } = await saveOrUpdate(markdown, { pageId, path: currentPagePath || currentPathname, revisionId: revisionIdHackmdSynced }, optionsToSave);
       await mutatePageData();
       await mutateTagsInfo();
 
@@ -125,6 +126,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
         await mutateCurrentPageId(page._id);
         await mutatePageData();
       }
+      setIsInitialized(false);
       mutateEditorMode(EditorMode.View);
     }
     catch (error) {
@@ -132,7 +134,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
       toastError(error.message);
     }
   // eslint-disable-next-line max-len
-  }, [editorMode, isSlackEnabled, currentPathname, slackChannels, grant, revision, pageTags, saveOrUpdate, pageId, currentPagePath, mutatePageData, mutateTagsInfo, isNotFound, mutateEditorMode, router, mutateCurrentPageId]);
+  }, [editorMode, isSlackEnabled, currentPathname, slackChannels, grant, revision, revisionIdHackmdSynced, pageTags, saveOrUpdate, pageId, currentPagePath, mutatePageData, mutateTagsInfo, isNotFound, mutateEditorMode, router, mutateCurrentPageId]);
 
   // set handler to save and reload Page
   useEffect(() => {

+ 9 - 2
packages/app/src/components/PageEditorByHackmd/HackmdEditor.jsx

@@ -1,6 +1,8 @@
 import React from 'react';
-import PropTypes from 'prop-types';
+
 import connectToChild from 'penpal/lib/connectToChild';
+import PropTypes from 'prop-types';
+
 import loggerFactory from '~/utils/logger';
 
 
@@ -27,11 +29,16 @@ export default class HackmdEditor extends React.PureComponent {
   }
 
   async initHackmdWithPenpal() {
+    const shouldInit = document.getElementById('iframe-hackmd') != null;
+    if (shouldInit) {
+      return;
+    }
+
     // eslint-disable-next-line @typescript-eslint/no-this-alias
     const _this = this; // for in methods scope
-
     const iframe = document.createElement('iframe');
     iframe.src = `${this.props.hackmdUri}/${this.props.pageIdOnHackmd}?both`;
+    iframe.id = 'iframe-hackmd';
     this.iframeContainer.appendChild(iframe);
 
     const connection = connectToChild({

+ 6 - 6
packages/app/src/components/PageStatusAlert.tsx

@@ -7,8 +7,9 @@ import { useEditingMarkdown, useIsConflict } from '~/stores/editor';
 import {
   useHasDraftOnHackmd, useIsHackmdDraftUpdatingInRealtime, useRevisionIdHackmdSynced,
 } from '~/stores/hackmd';
+import { useConflictDiffModal } from '~/stores/modal';
 import { useSWRxCurrentPage } from '~/stores/page';
-import { useRemoteRevisionId, useRemoteRevisionLastUpdatUser } from '~/stores/remote-latest-page';
+import { useRemoteRevisionId, useRemoteRevisionLastUpdateUser } from '~/stores/remote-latest-page';
 
 import { Username } from './User/Username';
 
@@ -27,11 +28,12 @@ export const PageStatusAlert = (): JSX.Element => {
   const { data: hasDraftOnHackmd } = useHasDraftOnHackmd();
   const { data: isConflict } = useIsConflict();
   const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
+  const { open: openConflictDiffModal } = useConflictDiffModal();
 
   // store remote latest page data
   const { data: revisionIdHackmdSynced } = useRevisionIdHackmdSynced();
   const { data: remoteRevisionId } = useRemoteRevisionId();
-  const { data: remoteRevisionLastUpdateUser } = useRemoteRevisionLastUpdatUser();
+  const { data: remoteRevisionLastUpdateUser } = useRemoteRevisionLastUpdateUser();
 
   const { data: pageData, mutate: mutatePageData } = useSWRxCurrentPage();
   const revision = pageData?.revision;
@@ -42,10 +44,8 @@ export const PageStatusAlert = (): JSX.Element => {
   }, [mutateEditingMarkdown, mutatePageData]);
 
   const onClickResolveConflict = useCallback(() => {
-    // this.props.pageContainer.setState({
-    //   isConflictDiffModalOpen: true,
-    // });
-  }, []);
+    openConflictDiffModal();
+  }, [openConflictDiffModal]);
 
   const getContentsForSomeoneEditingAlert = useCallback((): AlertComponentContents => {
     return {

+ 2 - 2
packages/app/src/components/PutbackPageModal.jsx

@@ -103,7 +103,7 @@ const PutBackPageModal = () => {
     return (
       <>
         <ApiErrorMessageList errs={errs} targetPath={targetPath} />
-        <button type="button" className="btn btn-info" onClick={putbackPageButtonHandler}>
+        <button type="button" className="btn btn-info" onClick={putbackPageButtonHandler} data-testid="put-back-execution-button">
           <i className="icon-action-undo mr-2" aria-hidden="true"></i> { t('Put Back') }
         </button>
       </>
@@ -116,7 +116,7 @@ const PutBackPageModal = () => {
   }, [closePutBackPageModal]);
 
   return (
-    <Modal isOpen={isOpened} toggle={closeModalHandler}>
+    <Modal isOpen={isOpened} toggle={closeModalHandler} data-testid="put-back-page-modal">
       <ModalHeader tag="h4" toggle={closeModalHandler} className="bg-info text-light">
         <HeaderContent/>
       </ModalHeader>

+ 1 - 1
packages/app/src/components/ReactMarkdownComponents/DrawioViewerWithEditButton.tsx

@@ -5,7 +5,7 @@ import EventEmitter from 'events';
 import {
   DrawioEditByViewerProps,
   DrawioViewer, DrawioViewerProps, extractCodeFromMxfile,
-} from '@growi/remark-drawio-plugin';
+} from '@growi/remark-drawio';
 import { useTranslation } from 'next-i18next';
 
 import { useIsGuestUser, useIsSharedUser, useShareLinkId } from '~/stores/context';

+ 1 - 1
packages/app/src/components/Script/DrawioViewerScript.tsx

@@ -1,6 +1,6 @@
 import { useCallback } from 'react';
 
-import type { IGraphViewerGlobal } from '@growi/remark-drawio-plugin';
+import type { IGraphViewerGlobal } from '@growi/remark-drawio';
 import Script from 'next/script';
 
 declare global {

+ 11 - 4
packages/app/src/components/Sidebar/PageTree/Item.tsx

@@ -14,6 +14,7 @@ import { bookmark, unbookmark, resumeRenameOperation } from '~/client/services/p
 import { toastWarning, toastError, toastSuccess } from '~/client/util/apiNotification';
 import { apiv3Put, apiv3Post } from '~/client/util/apiv3-client';
 import TriangleIcon from '~/components/Icons/TriangleIcon';
+import NotAvailableForGuest from '~/components/NotAvailableForGuest';
 import {
   IPageHasId, IPageInfoAll, IPageToDeleteWithMeta,
 } from '~/interfaces/page';
@@ -496,19 +497,25 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
           >
             {/* pass the color property to reactstrap dropdownToggle props. https://6-4-0--reactstrap.netlify.app/components/dropdowns/  */}
             <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control p-0 grw-visible-on-hover mr-1">
-              <i className="icon-options fa fa-rotate-90 p-1"></i>
+              <NotAvailableForGuest>
+                <i id='option-button-in-page-tree' className="icon-options fa fa-rotate-90 p-1"></i>
+              </NotAvailableForGuest>
             </DropdownToggle>
           </PageItemControl>
-          {!pagePathUtils.isUsersTopPage(page.path ?? '') && (
+        </div>
+
+        {!pagePathUtils.isUsersTopPage(page.path ?? '') && (
+          <NotAvailableForGuest>
             <button
+              id='page-create-button-in-page-tree'
               type="button"
               className="border-0 rounded btn btn-page-item-control p-0 grw-visible-on-hover"
               onClick={onClickPlusButton}
             >
               <i className="icon-plus d-block p-0" />
             </button>
-          )}
-        </div>
+          </NotAvailableForGuest>
+        )}
       </li>
 
       {isEnableActions && isNewPageInputShown && (

+ 1 - 4
packages/app/src/components/SubscribeButton.tsx

@@ -20,15 +20,12 @@ const SubscribeButton: FC<Props> = (props: Props) => {
   const isSubscribing = status === SubscriptionStatusType.SUBSCRIBE;
 
   const getTooltipMessage = useCallback(() => {
-    if (isGuestUser) {
-      return 'Not available for guest';
-    }
 
     if (isSubscribing) {
       return 'tooltip.stop_notification';
     }
     return 'tooltip.receive_notifications';
-  }, [isGuestUser, isSubscribing]);
+  }, [isSubscribing]);
 
   return (
     <>

+ 3 - 0
packages/app/src/interfaces/in-app-notification.ts

@@ -1,3 +1,5 @@
+import type { IPageSnapshot } from '~/models/serializers/in-app-notification-snapshot/page';
+
 import { IPage } from './page';
 import { IUser } from './user';
 
@@ -16,6 +18,7 @@ export interface IInAppNotification {
   actionUsers: IUser[]
   createdAt: Date
   snapshot: string
+  parsedSnapshot?: IPageSnapshot
 }
 
 /*

+ 1 - 0
packages/app/src/interfaces/plugin.ts

@@ -1,6 +1,7 @@
 export const GrowiPluginResourceType = {
   Template: 'template',
   Style: 'style',
+  Theme: 'theme',
   Script: 'script',
 } as const;
 export type GrowiPluginResourceType = typeof GrowiPluginResourceType[keyof typeof GrowiPluginResourceType];

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

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

+ 2 - 2
packages/app/src/models/serializers/in-app-notification-snapshot/page.ts

@@ -1,5 +1,5 @@
-import { IUser } from '~/interfaces/user';
-import { IPage } from '~/interfaces/page';
+import type { IPage } from '~/interfaces/page';
+import type { IUser } from '~/interfaces/user';
 
 export interface IPageSnapshot {
   path: string

+ 6 - 0
packages/app/src/pages/[[...path]].page.tsx

@@ -37,8 +37,10 @@ import type { PageModel, PageDocument } from '~/server/models/page';
 import type { PageRedirectModel } from '~/server/models/page-redirect';
 import type { UserUISettingsModel } from '~/server/models/user-ui-settings';
 import { useEditingMarkdown } from '~/stores/editor';
+import { useHasDraftOnHackmd, usePageIdOnHackmd, useRevisionIdHackmdSynced } from '~/stores/hackmd';
 import { useSWRxCurrentPage, useSWRxIsGrantNormalized } from '~/stores/page';
 import { useRedirectFrom } from '~/stores/page-redirect';
+import { useRemoteRevisionId } from '~/stores/remote-latest-page';
 import {
   useEditorMode, useSelectedGrant,
   usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser, useSidebarCollapsed, useCurrentSidebarContents, useCurrentProductNavWidth,
@@ -257,6 +259,10 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
   const pagePath = pageWithMeta?.data.path ?? (!_isPermalink(props.currentPathname) ? props.currentPathname : undefined);
 
   useCurrentPageId(pageId ?? null);
+  useRevisionIdHackmdSynced(pageWithMeta?.data.revisionHackmdSynced);
+  useRemoteRevisionId(pageWithMeta?.data.revision._id);
+  usePageIdOnHackmd(pageWithMeta?.data.pageIdOnHackmd);
+  useHasDraftOnHackmd(pageWithMeta?.data.hasDraftOnHackmd);
   // useIsNotCreatable(props.isForbidden || !isCreatablePage(pagePath)); // TODO: need to include props.isIdentical
   useCurrentPathname(props.currentPathname);
 

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

@@ -58,14 +58,14 @@ const HeadersForGrowiPlugin = (props: HeadersForGrowiPluginProps): JSX.Element =
           elements.push(<>
             {/* eslint-disable-next-line @next/next/no-sync-scripts */ }
             <script type="module" key={`script_${growiPlugin.installedPath}`}
-              src={`/plugins/${growiPlugin.installedPath}/dist/${manifest['client-entry.tsx'].file}`} />
+              src={`/static/plugins/${growiPlugin.installedPath}/dist/${manifest['client-entry.tsx'].file}`} />
           </>);
         }
         // add link
         if (types.includes(GrowiPluginResourceType.Script) || types.includes(GrowiPluginResourceType.Style)) {
           elements.push(<>
             <link rel="stylesheet" key={`link_${growiPlugin.installedPath}`}
-              href={`/plugins/${growiPlugin.installedPath}/dist/${manifest['client-entry.tsx'].css}`} />
+              href={`/static/plugins/${growiPlugin.installedPath}/dist/${manifest['client-entry.tsx'].css}`} />
           </>);
         }
 

+ 1 - 1
packages/app/src/server/crowi/express-init.js

@@ -120,7 +120,7 @@ module.exports = function(crowi, app) {
   app.use('/static/preset-themes', express.static(
     resolveFromRoot(`../../node_modules/@growi/preset-themes/${path.dirname(presetThemesManifestPath)}`),
   ));
-  app.use('/plugins', express.static(path.resolve(__dirname, '../../../tmp/plugins')));
+  app.use('/static/plugins', express.static(path.resolve(__dirname, '../../../tmp/plugins')));
 
   app.engine('html', swig.renderFile);
   // app.set('view cache', false);  // Default: true in production, otherwise undefined. -- 2017.07.04 Yuki Takei

+ 1 - 1
packages/app/src/server/routes/apiv3/index.js

@@ -104,7 +104,7 @@ module.exports = (crowi, app) => {
     userActivation.validateCompleteRegistration,
     userActivation.completeRegistrationAction(crowi));
 
-  router.use('/plugins-extension', require('./plugins-extension')(crowi));
+  router.use('/plugins', require('./plugins')(crowi));
 
   router.use('/user-ui-settings', require('./user-ui-settings')(crowi));
 

+ 1 - 2
packages/app/src/server/routes/apiv3/plugins-extension.ts → packages/app/src/server/routes/apiv3/plugins.ts

@@ -16,11 +16,10 @@ module.exports = (crowi: Crowi) => {
     }
 
     try {
-      await pluginService.install(crowi, req.body.pluginInstallerForm);
+      await pluginService.install(req.body.pluginInstallerForm);
       return res.apiv3({});
     }
     catch (err) {
-      // TODO: error handling
       return res.apiv3Err(err, 400);
     }
   });

+ 12 - 12
packages/app/src/server/routes/login.js

@@ -102,18 +102,18 @@ module.exports = function(crowi, app) {
 
   actions.preLogin = function(req, res, next) {
     // user has already logged in
-    // const { user } = req;
-    // if (user != null && user.status === User.STATUS_ACTIVE) {
-    //   const { redirectTo } = req.session;
-    //   // remove session.redirectTo
-    //   delete req.session.redirectTo;
-    //   return res.safeRedirect(redirectTo);
-    // }
-
-    // // set referer to 'redirectTo'
-    // if (req.session.redirectTo == null && req.headers.referer != null) {
-    //   req.session.redirectTo = req.headers.referer;
-    // }
+    const { user } = req;
+    if (user != null && user.status === User.STATUS_ACTIVE) {
+      const { redirectTo } = req.session;
+      // remove session.redirectTo
+      delete req.session.redirectTo;
+      return res.safeRedirect(redirectTo);
+    }
+
+    // set referer to 'redirectTo'
+    if (req.session.redirectTo == null && req.headers.referer != null) {
+      req.session.redirectTo = req.headers.referer;
+    }
 
     next();
   };

+ 66 - 38
packages/app/src/server/service/plugin.ts

@@ -1,16 +1,16 @@
-import { execSync } from 'child_process';
 import fs from 'fs';
 import path from 'path';
 
+// eslint-disable-next-line no-restricted-imports
+import axios from 'axios';
 import mongoose from 'mongoose';
+import streamToPromise from 'stream-to-promise';
+import unzipper from 'unzipper';
 
-import type { GrowiPlugin, GrowiPluginMeta, GrowiPluginOrigin } from '~/interfaces/plugin';
+import type { GrowiPlugin, GrowiPluginOrigin } from '~/interfaces/plugin';
 import loggerFactory from '~/utils/logger';
 import { resolveFromRoot } from '~/utils/project-dir-utils';
 
-// eslint-disable-next-line import/no-cycle
-import Crowi from '../crowi';
-
 const logger = loggerFactory('growi:plugins:plugin-utils');
 
 const pluginStoringPath = resolveFromRoot('tmp/plugins');
@@ -21,25 +21,12 @@ const githubReposIdPattern = new RegExp(/^\/([^/]+)\/([^/]+)$/);
 
 export class PluginService {
 
-  crowi: any;
-
-  growiBridgeService: any;
-
-  baseDir: any;
-
-  getFile:any;
-
-  constructor(crowi) {
-    this.crowi = crowi;
-    this.growiBridgeService = crowi.growiBridgeService;
-    this.baseDir = path.join(crowi.tmpDir, 'plugins');
-    this.getFile = this.growiBridgeService.getFile.bind(this);
-  }
-
-  async install(crowi: Crowi, origin: GrowiPluginOrigin): Promise<void> {
+  async install(origin: GrowiPluginOrigin): Promise<void> {
     // download
     const ghUrl = new URL(origin.url);
     const ghPathname = ghUrl.pathname;
+    // TODO: Branch names can be specified.
+    const ghBranch = 'main';
 
     const match = ghPathname.match(githubReposIdPattern);
     if (ghUrl.hostname !== 'github.com' || match == null) {
@@ -48,13 +35,10 @@ export class PluginService {
 
     const ghOrganizationName = match[1];
     const ghReposName = match[2];
+    const requestUrl = `https://github.com/${ghOrganizationName}/${ghReposName}/archive/refs/heads/${ghBranch}.zip`;
 
-    try {
-      await this.downloadZipFile(`${ghUrl.href}/archive/refs/heads/main.zip`, ghOrganizationName, ghReposName);
-    }
-    catch (err) {
-      console.log('downloadZipFile error', err);
-    }
+    // download github repository to local file system
+    await this.download(requestUrl, ghOrganizationName, ghReposName, ghBranch);
 
     // save plugin metadata
     const installedPath = `${ghOrganizationName}/${ghReposName}`;
@@ -64,18 +48,63 @@ export class PluginService {
     return;
   }
 
-  async downloadZipFile(url: string, ghOrganizationName: string, ghReposName: string): Promise<void> {
+  async download(requestUrl: string, ghOrganizationName: string, ghReposName: string, ghBranch: string): Promise<void> {
 
-    const downloadTargetPath = pluginStoringPath;
-    const zipFilePath = path.join(downloadTargetPath, 'main.zip');
-    const unzipTargetPath = path.join(pluginStoringPath, ghOrganizationName);
+    const zipFilePath = path.join(pluginStoringPath, `${ghBranch}.zip`);
+    const unzippedPath = path.join(pluginStoringPath, ghOrganizationName);
 
-    const stdout1 = execSync(`wget ${url} -O ${zipFilePath}`);
-    const stdout2 = execSync(`mkdir -p ${ghOrganizationName}`);
-    const stdout3 = execSync(`rm -rf ${ghOrganizationName}/${ghReposName}`);
-    const stdout4 = execSync(`unzip ${zipFilePath} -d ${unzipTargetPath}`);
-    const stdout5 = execSync(`mv ${unzipTargetPath}/${ghReposName}-main ${unzipTargetPath}/${ghReposName}`);
-    const stdout6 = execSync(`rm ${zipFilePath}`);
+    const renamePath = async(oldPath: fs.PathLike, newPath: fs.PathLike) => {
+      fs.renameSync(oldPath, newPath);
+    };
+
+    const downloadFile = async(requestUrl: string, filePath: string) => {
+      return new Promise<void>((resolve, reject) => {
+        axios({
+          method: 'GET',
+          url: requestUrl,
+          responseType: 'stream',
+        })
+          .then((res) => {
+            if (res.status === 200) {
+              const file = fs.createWriteStream(filePath);
+              res.data.pipe(file)
+                .on('close', () => file.close())
+                .on('finish', () => {
+                  return resolve();
+                });
+            }
+            else {
+              return reject(res.status);
+            }
+          }).catch((err) => {
+            return reject(err);
+          });
+      });
+    };
+
+    const unzip = async(zipFilePath: fs.PathLike, unzippedPath: fs.PathLike) => {
+      const stream = fs.createReadStream(zipFilePath);
+      const unzipStream = stream.pipe(unzipper.Extract({ path: unzippedPath }));
+      const deleteZipFile = (path: fs.PathLike) => fs.unlink(path, (err) => { return err });
+
+      try {
+        await streamToPromise(unzipStream);
+        deleteZipFile(zipFilePath);
+      }
+      catch (err) {
+        return err;
+      }
+    };
+
+    try {
+      await downloadFile(requestUrl, zipFilePath);
+      await unzip(zipFilePath, unzippedPath);
+      await renamePath(`${unzippedPath}/${ghReposName}-${ghBranch}`, `${unzippedPath}/${ghReposName}`);
+    }
+    catch (err) {
+      logger.error(err);
+      throw new Error(err);
+    }
 
     return;
   }
@@ -135,5 +164,4 @@ export class PluginService {
     return [];
   }
 
-
 }

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

@@ -2,8 +2,8 @@
 import { ComponentType } from 'react';
 
 import { isClient } from '@growi/core';
-import * as drawioPlugin from '@growi/remark-drawio-plugin';
-import growiPlugin from '@growi/remark-growi-plugin';
+import * as drawioPlugin from '@growi/remark-drawio';
+import growiDirective from '@growi/remark-growi-directive';
 import { Lsx, LsxImmutable } from '@growi/remark-lsx/components';
 import * as lsxGrowiPlugin from '@growi/remark-lsx/services/renderer';
 import { Schema as SanitizeOption } from 'hast-util-sanitize';
@@ -296,7 +296,7 @@ const generateCommonOptions = (pagePath: string|undefined, config: RendererConfi
       gfm,
       emoji,
       pukiwikiLikeLinker,
-      growiPlugin,
+      growiDirective,
     ],
     rehypePlugins: [
       [relativeLinksByPukiwikiLikeLinker, { pagePath }],

+ 21 - 2
packages/app/src/stores/in-app-notification.ts

@@ -1,7 +1,15 @@
 import useSWR, { SWRResponse } from 'swr';
-import { InAppNotificationStatuses, IInAppNotification, PaginateResult } from '~/interfaces/in-app-notification';
+
+import type { InAppNotificationStatuses, IInAppNotification, PaginateResult } from '~/interfaces/in-app-notification';
+import { parseSnapshot } from '~/models/serializers/in-app-notification-snapshot/page';
+import loggerFactory from '~/utils/logger';
+
 import { apiv3Get } from '../client/util/apiv3-client';
 
+const logger = loggerFactory('growi:cli:InAppNotification');
+
+type inAppNotificationPaginateResult = PaginateResult<IInAppNotification>
+
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
 export const useSWRxInAppNotifications = <Data, Error>(
   limit: number,
@@ -10,7 +18,18 @@ export const useSWRxInAppNotifications = <Data, Error>(
 ): SWRResponse<PaginateResult<IInAppNotification>, Error> => {
   return useSWR(
     ['/in-app-notification/list', limit, offset, status],
-    endpoint => apiv3Get(endpoint, { limit, offset, status }).then(response => response.data),
+    endpoint => apiv3Get(endpoint, { limit, offset, status }).then((response) => {
+      const inAppNotificationPaginateResult = response.data as inAppNotificationPaginateResult;
+      inAppNotificationPaginateResult.docs.forEach((doc) => {
+        try {
+          doc.parsedSnapshot = parseSnapshot(doc.snapshot as string);
+        }
+        catch (err) {
+          logger.warn('Failed to parse snapshot', err);
+        }
+      });
+      return inAppNotificationPaginateResult;
+    }),
   );
 };
 

+ 27 - 0
packages/app/src/stores/modal.tsx

@@ -553,3 +553,30 @@ export const useHandsontableModal = (status?: HandsontableModalStatus): SWRRespo
     close,
   };
 };
+
+/*
+ * ConflictDiffModal
+ */
+type ConflictDiffModalStatus = {
+  isOpened: boolean,
+}
+
+type ConflictDiffModalUtils = {
+  open(): void,
+  close(): void,
+}
+
+export const useConflictDiffModal = (): SWRResponse<ConflictDiffModalStatus, Error> & ConflictDiffModalUtils => {
+
+  const initialStatus: ConflictDiffModalStatus = { isOpened: false };
+  const swrResponse = useStaticSWR<ConflictDiffModalStatus, Error>('conflictDiffModal', undefined, { fallbackData: initialStatus });
+
+  return Object.assign(swrResponse, {
+    open: () => {
+      swrResponse.mutate({ isOpened: true });
+    },
+    close: () => {
+      swrResponse.mutate({ isOpened: false });
+    },
+  });
+};

+ 45 - 3
packages/app/src/stores/remote-latest-page.ts

@@ -2,6 +2,7 @@ import { SWRResponse } from 'swr';
 
 import { IUser } from '~/interfaces/user';
 
+import { useRevisionIdHackmdSynced, useHasDraftOnHackmd } from './hackmd';
 import { useStaticSWR } from './use-static-swr';
 
 
@@ -10,9 +11,50 @@ export const useRemoteRevisionId = (initialData?: string): SWRResponse<string, E
 };
 
 export const useRemoteRevisionBody = (initialData?: string): SWRResponse<string, Error> => {
-  return useStaticSWR<string, Error>('remoteRevisionId', initialData);
+  return useStaticSWR<string, Error>('remoteRevisionBody', initialData);
+};
+
+export const useRemoteRevisionLastUpdateUser = (initialData?: IUser): SWRResponse<IUser, Error> => {
+  return useStaticSWR<IUser, Error>('remoteRevisionLastUpdateUser', initialData);
+};
+
+export const useRemoteRevisionLastUpdatedAt = (initialData?: Date): SWRResponse<Date, Error> => {
+  return useStaticSWR<Date, Error>('remoteRevisionLastUpdatedAt', initialData);
 };
 
-export const useRemoteRevisionLastUpdatUser = (initialData?: IUser): SWRResponse<IUser, Error> => {
-  return useStaticSWR<IUser, Error>('remoteRevisionLastUpdatUser', initialData);
+type RemoteRevisionData = {
+  remoteRevisionId: string,
+  remoteRevisionBody: string,
+  remoteRevisionLastUpdateUser: IUser,
+  remoteRevisionLastUpdatedAt: Date,
+  revisionIdHackmdSynced: string,
+  hasDraftOnHackmd: boolean,
+}
+
+
+// set remote data all at once
+export const useSetRemoteLatestPageData = (): { setRemoteLatestPageData: (pageData: RemoteRevisionData) => void } => {
+  const { mutate: mutateRemoteRevisionId } = useRemoteRevisionId();
+  const { mutate: mutateRemoteRevisionBody } = useRemoteRevisionBody();
+  const { mutate: mutateRemoteRevisionLastUpdateUser } = useRemoteRevisionLastUpdateUser();
+  const { mutate: mutateRemoteRevisionLastUpdatedAt } = useRemoteRevisionLastUpdatedAt();
+  const { mutate: mutateRevisionIdHackmdSynced } = useRevisionIdHackmdSynced();
+  const { mutate: mutateHasDraftOnHackmd } = useHasDraftOnHackmd();
+
+  const setRemoteLatestPageData = (remoteRevisionData: RemoteRevisionData) => {
+    const {
+      remoteRevisionId, remoteRevisionBody, remoteRevisionLastUpdateUser, remoteRevisionLastUpdatedAt, revisionIdHackmdSynced, hasDraftOnHackmd,
+    } = remoteRevisionData;
+    mutateRemoteRevisionId(remoteRevisionId);
+    mutateRemoteRevisionBody(remoteRevisionBody);
+    mutateRemoteRevisionLastUpdateUser(remoteRevisionLastUpdateUser);
+    mutateRemoteRevisionLastUpdatedAt(remoteRevisionLastUpdatedAt);
+    mutateRevisionIdHackmdSynced(revisionIdHackmdSynced);
+    mutateHasDraftOnHackmd(hasDraftOnHackmd);
+  };
+
+  return {
+    setRemoteLatestPageData,
+  };
+
 };

+ 6 - 0
packages/app/src/styles/bootstrap/_variables.scss

@@ -36,6 +36,12 @@ $red: #ff0a54 !default;
 
 $enable-shadows: true;
 
+// Links
+//
+// Style anchor elements.
+
+$link-hover-decoration: none !default;
+
 // Grid breakpoints
 //
 // Define the minimum dimensions at which your layout will change,

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

@@ -553,11 +553,11 @@ body.editing-sidebar {
 }
 
 .grid-preview-col-1 {
-  background: $info;
+  background: var(--info);
 }
 
 .grid-preview-col-2 {
-  background: $success;
+  background: var(--success);
 }
 
 .grid-preview-col-3 {

+ 1 - 1
packages/app/src/styles/theme/_reboot-bootstrap-border-colors.scss

@@ -25,5 +25,5 @@
 }
 
 .border-info {
-  border-color: $info !important;
+  border-color: ver(--info) !important;
 }

+ 4 - 4
packages/app/src/styles/theme/_reboot-toastr-colors.scss

@@ -1,15 +1,15 @@
 .toast-success {
-  background-color: $success;
+  background-color: var(--success);
 }
 
 .toast-error {
-  background-color: $danger;
+  background-color: var(--danger);
 }
 
 .toast-info {
-  background-color: $info;
+  background-color: var(--info);
 }
 
 .toast-warning {
-  background-color: $warning;
+  background-color: var(--warning);
 }

+ 17 - 5
packages/app/test/cypress/integration/20-basic-features/20-basic-features--use-tools.spec.ts

@@ -132,16 +132,28 @@ context('Modal for page operation', () => {
     cy.screenshot(`${ssPrefix}create-template-for-descendants-error`);
   });
 
-  it('PageDeleteModal is shown successfully', () => {
+  it('Page Deletion and PutBack is executed successfully', () => {
     cy.visit('/Sandbox/Bootstrap4');
     cy.waitUntilSkeletonDisappear();
 
-     cy.get('#grw-subnav-container').within(() => {
-       cy.getByTestid('open-page-item-control-btn').click({force: true});
-       cy.getByTestid('open-page-delete-modal-btn').click({force: true});
+    cy.get('#grw-subnav-container').within(() => {
+      cy.getByTestid('open-page-item-control-btn').click({force: true});
+      cy.getByTestid('open-page-delete-modal-btn').click({force: true});
     });
 
-     cy.getByTestid('page-delete-modal').should('be.visible').screenshot(`${ssPrefix}-delete-bootstrap4`);
+    cy.getByTestid('page-delete-modal').should('be.visible').within(() => {
+      cy.screenshot(`${ssPrefix}-delete-modal`);
+      cy.getByTestid('delete-page-button').click();
+    });
+    cy.getByTestid('trash-page-alert').should('be.visible');
+    cy.screenshot(`${ssPrefix}-bootstrap4-is-in-garbage-box`);
+
+    cy.getByTestid('put-back-button').click();
+    cy.getByTestid('put-back-page-modal').should('be.visible').within(() => {
+      cy.screenshot(`${ssPrefix}-put-back-modal`);
+      cy.getByTestid('put-back-execution-button').should('be.visible').click();
+    });
+    cy.screenshot(`${ssPrefix}-put-backed-bootstrap4-page`);
   });
 
   it('PageDuplicateModal is shown successfully', () => {

+ 2 - 0
packages/app/test/cypress/integration/40-admin/40-admin--access-to-admin-page.spec.ts

@@ -32,6 +32,8 @@ context('Access to Admin page', () => {
   it('/admin/app is successfully loaded', () => {
     cy.visit('/admin/app');
     cy.getByTestid('admin-app-settings').should('be.visible');
+    cy.getByTestid('v5-page-migration').should('be.visible');
+    cy.get('#cbFileUpload').should('be.checked')
     cy.screenshot(`${ssPrefix}-admin-app`);
   });
 

+ 0 - 1
packages/hackmd/src/hackmd-styles.js

@@ -19,7 +19,6 @@ const styles = '<%= styles %>'; // will be replaced by ejs
  */
 function insertStyle() {
   const element = document.createElement('style');
-  element.type = 'text/css';
   element.appendChild(document.createTextNode(unescape(styles)));
   document.getElementsByTagName('head')[0].appendChild(element);
 }

+ 1 - 1
packages/hackmd/src/index.js

@@ -11,5 +11,5 @@ const stylesCSSFile = fs.readFileSync(path.resolve(__dirname, `${dirPath}/styles
 module.exports = {
   stylesJS: stylesJSFile.toString(),
   agentJS: agentJSFile.toString(),
-  stylesCSS: stylesCSSFile.toString(),
+  stylesCSS: stylesCSSFile.toString().replace(/(\r\n|\n|\r)/gm, ''), // https://stackoverflow.com/questions/10805125/how-to-remove-all-line-breaks-from-a-string
 };

+ 6 - 0
packages/preset-themes/src/styles/bootstrap/_variables.scss

@@ -50,6 +50,12 @@ $grid-breakpoints: (
   xxl: 1480px,
 );
 
+// Links
+//
+// Style anchor elements.
+
+$link-hover-decoration: none !default;
+
 // Grid containers
 //
 // Define the maximum width of `.container` for different screen sizes.

+ 4 - 4
packages/preset-themes/src/styles/default.scss

@@ -7,10 +7,10 @@
 
 // colors for overriding bootstrap $theme-colors
 // $secondary: #;
-// $info: #;
-// $success: #;
-// $warning: #;
-// $danger: #;
+// --info: #;
+// --success: #;
+// --warning: #;
+// --danger: #;
 // $light: #;
 // $dark: #;
 

+ 0 - 8
packages/preset-themes/src/styles/theme/_apply-colors.scss

@@ -499,14 +499,6 @@ body.editing-sidebar {
   background: var.$growi-blue;
 }
 
-.grid-preview-col-1 {
-  background: $info;
-}
-
-.grid-preview-col-2 {
-  background: $success;
-}
-
 .grid-preview-col-3 {
   background: var.$growi-green;
 }

+ 0 - 4
packages/preset-themes/src/styles/theme/_reboot-bootstrap-border-colors.scss

@@ -23,7 +23,3 @@
 .border-left {
   border-left: $border-width solid $border-color !important;
 }
-
-.border-info {
-  border-color: $info !important;
-}

+ 0 - 15
packages/preset-themes/src/styles/theme/_reboot-toastr-colors.scss

@@ -1,15 +0,0 @@
-.toast-success {
-  background-color: $success;
-}
-
-.toast-error {
-  background-color: $danger;
-}
-
-.toast-info {
-  background-color: $info;
-}
-
-.toast-warning {
-  background-color: $warning;
-}

+ 0 - 0
packages/remark-drawio-plugin/.eslintignore → packages/remark-drawio/.eslintignore


+ 0 - 0
packages/remark-drawio-plugin/.eslintrc.js → packages/remark-drawio/.eslintrc.js


+ 0 - 0
packages/remark-drawio-plugin/.gitignore → packages/remark-drawio/.gitignore


+ 1 - 1
packages/remark-drawio-plugin/README.md → packages/remark-drawio/README.md

@@ -1,4 +1,4 @@
-# remark-drawio-plugin
+# remark-drawio
 
 [GROWI][growi] remark plugin to draw diagrams with [draw.io (diagrams.net)](https://www.diagrams.net/)
 

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

@@ -1,5 +1,5 @@
 {
-  "name": "@growi/remark-drawio-plugin",
+  "name": "@growi/remark-drawio",
   "version": "6.0.0-RC.9",
   "description": "remark plugin to draw diagrams with draw.io (diagrams.net)",
   "license": "MIT",

+ 0 - 0
packages/remark-drawio-plugin/src/components/DrawioViewer.module.scss → packages/remark-drawio/src/components/DrawioViewer.module.scss


+ 0 - 0
packages/remark-drawio-plugin/src/components/DrawioViewer.tsx → packages/remark-drawio/src/components/DrawioViewer.tsx


+ 1 - 1
packages/remark-drawio-plugin/src/index.ts → packages/remark-drawio/src/index.ts

@@ -1,5 +1,5 @@
 export * from './interfaces/graph-viewer';
 export * from './components/DrawioViewer';
-export * from './services/renderer/remark-drawio-plugin';
+export * from './services/renderer/remark-drawio';
 export * from './utils/embed';
 export * from './utils/global';

+ 0 - 0
packages/remark-drawio-plugin/src/interfaces/graph-viewer.ts → packages/remark-drawio/src/interfaces/graph-viewer.ts


+ 0 - 0
packages/remark-drawio-plugin/src/services/renderer/remark-drawio-plugin.ts → packages/remark-drawio/src/services/renderer/remark-drawio.ts


+ 0 - 0
packages/remark-drawio-plugin/src/utils/embed.ts → packages/remark-drawio/src/utils/embed.ts


+ 0 - 0
packages/remark-drawio-plugin/src/utils/global.ts → packages/remark-drawio/src/utils/global.ts


+ 0 - 0
packages/remark-drawio-plugin/tsconfig.base.json → packages/remark-drawio/tsconfig.base.json


+ 0 - 0
packages/remark-drawio-plugin/tsconfig.build.json → packages/remark-drawio/tsconfig.build.json


+ 0 - 0
packages/remark-drawio-plugin/tsconfig.json → packages/remark-drawio/tsconfig.json


+ 0 - 0
packages/remark-growi-plugin/.eslintignore → packages/remark-growi-directive/.eslintignore


+ 0 - 0
packages/remark-growi-plugin/.eslintrc.cjs → packages/remark-growi-directive/.eslintrc.cjs


+ 0 - 0
packages/remark-growi-plugin/.gitignore → packages/remark-growi-directive/.gitignore


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

@@ -1,5 +1,5 @@
 {
-  "name": "@growi/remark-growi-plugin",
+  "name": "@growi/remark-growi-directive",
   "version": "6.0.0-RC.9",
   "description": "remark plugin to support GROWI plugin (forked from remark-directive@2.0.1)",
   "license": "MIT",

+ 0 - 0
packages/remark-growi-plugin/readme.md → packages/remark-growi-directive/readme.md


+ 6 - 0
packages/remark-growi-directive/src/index.js

@@ -0,0 +1,6 @@
+import { DirectiveType } from './mdast-util-growi-directive/consts.js';
+import { remarkGrowiDirectivePlugin } from './remark-growi-directive.js';
+
+export { DirectiveType as remarkGrowiDirectivePluginType };
+
+export default remarkGrowiDirectivePlugin;

+ 0 - 0
packages/remark-growi-plugin/src/mdast-util-growi-plugin/complex-types.d.ts → packages/remark-growi-directive/src/mdast-util-growi-directive/complex-types.d.ts


+ 0 - 0
packages/remark-growi-plugin/src/mdast-util-growi-plugin/consts.js → packages/remark-growi-directive/src/mdast-util-growi-directive/consts.js


+ 0 - 0
packages/remark-growi-plugin/src/mdast-util-growi-plugin/index.js → packages/remark-growi-directive/src/mdast-util-growi-directive/index.js


+ 0 - 0
packages/remark-growi-plugin/src/mdast-util-growi-plugin/readme.md → packages/remark-growi-directive/src/mdast-util-growi-directive/readme.md


+ 0 - 0
packages/remark-growi-plugin/src/micromark-extension-growi-plugin/index.js → packages/remark-growi-directive/src/micromark-extension-growi-directive/index.js


+ 0 - 0
packages/remark-growi-plugin/src/micromark-extension-growi-plugin/lib/directive-leaf.js → packages/remark-growi-directive/src/micromark-extension-growi-directive/lib/directive-leaf.js


+ 0 - 0
packages/remark-growi-plugin/src/micromark-extension-growi-plugin/lib/directive-text.js → packages/remark-growi-directive/src/micromark-extension-growi-directive/lib/directive-text.js


+ 0 - 0
packages/remark-growi-plugin/src/micromark-extension-growi-plugin/lib/factory-attributes.js → packages/remark-growi-directive/src/micromark-extension-growi-directive/lib/factory-attributes.js


+ 0 - 0
packages/remark-growi-plugin/src/micromark-extension-growi-plugin/lib/factory-label.js → packages/remark-growi-directive/src/micromark-extension-growi-directive/lib/factory-label.js


+ 0 - 0
packages/remark-growi-plugin/src/micromark-extension-growi-plugin/lib/factory-name.js → packages/remark-growi-directive/src/micromark-extension-growi-directive/lib/factory-name.js


+ 1 - 1
packages/remark-growi-plugin/src/micromark-extension-growi-plugin/lib/html.js → packages/remark-growi-directive/src/micromark-extension-growi-directive/lib/html.js

@@ -23,7 +23,7 @@
 import { parseEntities } from 'parse-entities';
 import { ok as assert } from 'uvu/assert';
 
-import { DirectiveType } from '../../mdast-util-growi-plugin/consts.js';
+import { DirectiveType } from '../../mdast-util-growi-directive/consts.js';
 
 const own = {}.hasOwnProperty;
 

+ 0 - 0
packages/remark-growi-plugin/src/micromark-extension-growi-plugin/lib/syntax.js → packages/remark-growi-directive/src/micromark-extension-growi-directive/lib/syntax.js


+ 0 - 0
packages/remark-growi-plugin/src/micromark-extension-growi-plugin/readme.md → packages/remark-growi-directive/src/micromark-extension-growi-directive/readme.md


+ 0 - 0
packages/remark-growi-plugin/src/micromark-factory-attributes-devider/index.d.ts → packages/remark-growi-directive/src/micromark-factory-attributes-devider/index.d.ts


+ 0 - 0
packages/remark-growi-plugin/src/micromark-factory-attributes-devider/index.js → packages/remark-growi-directive/src/micromark-factory-attributes-devider/index.js


+ 0 - 0
packages/remark-growi-plugin/src/micromark-factory-attributes-devider/readme.md → packages/remark-growi-directive/src/micromark-factory-attributes-devider/readme.md


+ 3 - 3
packages/remark-growi-plugin/src/remark-growi-plugin.js → packages/remark-growi-directive/src/remark-growi-directive.js

@@ -4,15 +4,15 @@
  * @typedef {import('mdast-util-directive')} DoNotTouchAsThisImportIncludesDirectivesInTree
  */
 
-import { directiveFromMarkdown, directiveToMarkdown } from './mdast-util-growi-plugin/index.js';
-import { directive } from './micromark-extension-growi-plugin/index.js';
+import { directiveFromMarkdown, directiveToMarkdown } from './mdast-util-growi-directive/index.js';
+import { directive } from './micromark-extension-growi-directive/index.js';
 
 /**
     * Plugin to support GROWI plugin (`$lsx(/path, depth=2)`).
     *
     * @type {import('unified').Plugin<void[], Root>}
     */
-export function remarkGrowiPlugin() {
+export function remarkGrowiDirectivePlugin() {
   const data = this.data();
 
   add('micromarkExtensions', directive());

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