瀏覽代碼

Merge branch 'dev/7.0.x' into imprv/141126-141349-design-of-input-field-for-slack-channel

reiji-h 2 年之前
父節點
當前提交
5d0b9876ad
共有 31 個文件被更改,包括 1033 次插入535 次删除
  1. 66 65
      apps/app/public/static/locales/en_US/translation.json
  2. 64 63
      apps/app/public/static/locales/ja_JP/translation.json
  3. 367 366
      apps/app/public/static/locales/zh_CN/translation.json
  4. 7 0
      apps/app/src/components/LoadingSpinner.jsx
  5. 39 0
      apps/app/src/components/LoadingSpinner.module.scss
  6. 11 5
      apps/app/src/components/LoginForm.tsx
  7. 6 0
      apps/app/src/components/PageEditor/ConflictDiffModal.module.scss
  8. 323 0
      apps/app/src/components/PageEditor/ConflictDiffModal.tsx
  9. 2 2
      apps/app/src/components/PageEditor/EditorNavbarBottom.tsx
  10. 1 1
      apps/app/src/components/PageEditor/OptionsSelector.tsx
  11. 2 2
      apps/app/src/components/PageEditor/PageEditor.tsx
  12. 3 14
      apps/app/src/components/Sidebar/RecentChanges/RecentChangesSubstance.module.scss
  13. 6 4
      apps/app/src/components/Sidebar/RecentChanges/RecentChangesSubstance.tsx
  14. 1 1
      apps/app/src/server/models/obsolete-page.js
  15. 2 1
      apps/app/src/server/routes/apiv3/page/update-page.ts
  16. 0 1
      apps/app/src/styles/_editor.scss
  17. 6 2
      packages/core/scss/_flex-expand.scss
  18. 0 1
      packages/custom-icons/svg/drawer_io.svg
  19. 0 1
      packages/custom-icons/svg/header.svg
  20. 3 1
      packages/editor/package.json
  21. 1 1
      packages/editor/src/components/CodeMirrorEditor/CodeMirrorEditor.tsx
  22. 21 0
      packages/editor/src/components/CodeMirrorEditor/Toolbar/AttachmentsDropup.module.scss
  23. 6 1
      packages/editor/src/components/CodeMirrorEditor/Toolbar/AttachmentsDropup.tsx
  24. 3 1
      packages/editor/src/components/CodeMirrorEditor/Toolbar/DiagramButton.tsx
  25. 3 1
      packages/editor/src/components/CodeMirrorEditor/Toolbar/TextFormatTools.tsx
  26. 1 1
      packages/editor/src/components/CodeMirrorEditor/Toolbar/Toolbar.tsx
  27. 38 0
      packages/editor/src/components/CodeMirrorEditorDiff.tsx
  28. 40 0
      packages/editor/src/components/MergeViewer.tsx
  29. 2 0
      packages/editor/src/components/index.ts
  30. 1 0
      packages/editor/src/consts/global-code-mirror-editor-key.ts
  31. 8 0
      yarn.lock

+ 66 - 65
apps/app/public/static/locales/en_US/translation.json

@@ -10,12 +10,12 @@
   "Duplicate": "Duplicate",
   "PathRecovery": "Path recovery",
   "Copy": "Copy",
-  "preview":"Preview",
-  "desktop":"Desktop",
-  "phone":"Smartphone",
-  "tablet":"Tablet",
+  "preview": "Preview",
+  "desktop": "Desktop",
+  "phone": "Smartphone",
+  "tablet": "Tablet",
   "Click to copy": "Click to copy",
-  "Rename" : "Rename",
+  "Rename": "Rename",
   "Move/Rename": "Move/Rename",
   "Redirected": "Redirected",
   "Unlinked": "Unlinked",
@@ -145,9 +145,9 @@
   "Recent Changes": "Recent Changes",
   "Page Tree": "Page Tree",
   "In-App Notification": "Notifications",
-  "original_path":"Original path",
-  "new_path":"New path",
-  "duplicated_path":"Duplicated path",
+  "original_path": "Original path",
+  "new_path": "New path",
+  "duplicated_path": "Duplicated path",
   "Link sharing is disabled": "Link sharing is disabled",
   "successfully_saved_the_page": "Successfully saved the page",
   "you_can_not_create_page_with_this_name": "You can not create page with this name",
@@ -213,10 +213,10 @@
   "Password": "Password",
   "Password Settings": "Password settings",
   "personal_settings": {
-  "disassociate_external_account": "Disassociate External Account",
-  "disassociate_external_account_desc": "Are you sure to disassociate the <strong>{{providerType}}</strong> account <strong>{{accountId}}</strong>?",
-  "set_new_password": "Set new Password",
-  "update_password": "Update password",
+    "disassociate_external_account": "Disassociate External Account",
+    "disassociate_external_account_desc": "Are you sure to disassociate the <strong>{{providerType}}</strong> account <strong>{{accountId}}</strong>?",
+    "set_new_password": "Set new Password",
+    "update_password": "Update password",
     "current_password": "Current password",
     "new_password": "New password",
     "new_password_confirm": "Re-enter new password",
@@ -226,7 +226,7 @@
     "Shere this page link to public": "Shere this page link to public",
     "share_link_list": "Share link list",
     "share_link_management": "Share Link Management",
-    "delete_all_share_links":"Delete all share links",
+    "delete_all_share_links": "Delete all share links",
     "expire": "Expiration",
     "Days": "Days",
     "Custom": "Custom",
@@ -234,8 +234,8 @@
     "enter_desc": "Enter description",
     "Unlimited": "unlimited",
     "Issue": "Issue",
-    "share_settings" :"Share settings",
-    "Invalid_Number_of_Date" : "You entered invalid value",
+    "share_settings": "Share settings",
+    "Invalid_Number_of_Date": "You entered invalid value",
     "link_sharing_is_disabled": "Link sharing is disabled"
   },
   "API Settings": "API settings",
@@ -307,7 +307,7 @@
       "stale": "More than {{count}} year has passed since last update.",
       "stale_plural": "More than {{count}} years has passed since last update.",
       "expiration": "This share link will expire at <strong>{{expiredAt}}</strong>.",
-      "no_deadline":"This page has no expiration date"
+      "no_deadline": "This page has no expiration date"
     }
   },
   "page_edit": {
@@ -342,8 +342,8 @@
     "comparing_source": "Source",
     "comparing_target": "Target",
     "comparing_revisions": "Comparing the difference",
-    "compare_latest":"Compare latest revision",
-    "compare_previous":"Compare previous revision"
+    "compare_latest": "Compare latest revision",
+    "compare_previous": "Compare previous revision"
   },
   "modal_rename": {
     "label": {
@@ -381,7 +381,7 @@
   "deleted_pages_completely": "{{path}} has been deleted completely",
   "renamed_pages": "{{path}} has been renamed",
   "empty_trash": "The trash has been emptied",
-  "modal_empty":{
+  "modal_empty": {
     "empty_the_trash": "Empty The Trash",
     "empty_the_trash_button": "Empty The Trash",
     "not_deletable_notice": "Some pages cannot be removed due to lack of permission.",
@@ -441,10 +441,9 @@
     "file_conflicting_with_newer_remote": "This file is conflicting with newer remote file",
     "resolve_conflict_message": "Please select page body",
     "resolve_conflict": "Resolve Conflict",
-    "resolve_and_save" : "Resolve and save",
-    "select_revision" : "Select {{revision}}",
+    "resolve_and_save": "Resolve and save",
+    "select_revision": "Select {{revision}}",
     "requested_revision": "mine",
-    "origin_revision": "origin",
     "latest_revision": "theirs",
     "selected_editable_revision": "Selected Page Body (Editable)"
   },
@@ -471,7 +470,7 @@
     "issue_share_link": "Succeeded to issue new share link",
     "remove_share_link": "Succeeded to remove {{count}} share links",
     "switch_disable_link_sharing_success": "Succeeded to update share link setting",
-    "failed_to_reset_password":"Failed to reset password",
+    "failed_to_reset_password": "Failed to reset password",
     "save_succeeded": "Saved successfully"
   },
   "template": {
@@ -537,13 +536,13 @@
     "check_all": "Check all",
     "deletion_modal_header": "Delete page",
     "delete_completely": "Delete completely",
-    "include_certain_path" : "Include {{pathToInclude}} path ",
-    "delete_all_selected_page" : "Delete All",
-    "currently_not_implemented":"This is not currently implemented",
-    "search_again" : "Search again",
-    "number_of_list_to_display" : "Display",
-    "page_number_unit" : "pages",
-    "hit_number_unit" : "hit",
+    "include_certain_path": "Include {{pathToInclude}} path ",
+    "delete_all_selected_page": "Delete All",
+    "currently_not_implemented": "This is not currently implemented",
+    "search_again": "Search again",
+    "number_of_list_to_display": "Display",
+    "page_number_unit": "pages",
+    "hit_number_unit": "hit",
     "sort_axis": {
       "relationScore": "Sort by relevance",
       "createdAt": "Creation date",
@@ -590,7 +589,7 @@
     "sign_in_error": "Login error",
     "registration_successful": "Registration successful. Please wait for administrator approval.",
     "Setup": "Setup",
-    "enabled_ldap_has_configuration_problem":"LDAP is enabled but the configuration has something wrong.",
+    "enabled_ldap_has_configuration_problem": "LDAP is enabled but the configuration has something wrong.",
     "set_env_var_for_logs": "(Please set the environment variables <code>DEBUG=crowi:service:PassportService</code> to get the logs)"
   },
   "invited": {
@@ -617,21 +616,21 @@
     "aws_sttings_required": "AWS settings required to use this function. Please ask the administrator.",
     "application_already_installed": "Application already installed.",
     "email_address_could_not_be_used": "This email address could not be used. (Make sure the allowed email address)",
-    "user_id_is_not_available":"This User ID is not available.",
-    "username_should_not_be_null":"Username should not be null. Please check Authentication Mechanism Settings on admin page",
-    "email_address_is_already_registered":"This email address is already registered.",
-    "can_not_register_maximum_number_of_users":"Can not register more than the maximum number of users.",
-    "email_settings_is_not_setup":"E-mail settings is not set up. Please ask the administrator.",
+    "user_id_is_not_available": "This User ID is not available.",
+    "username_should_not_be_null": "Username should not be null. Please check Authentication Mechanism Settings on admin page",
+    "email_address_is_already_registered": "This email address is already registered.",
+    "can_not_register_maximum_number_of_users": "Can not register more than the maximum number of users.",
+    "email_settings_is_not_setup": "E-mail settings is not set up. Please ask the administrator.",
     "email_authentication_is_not_enabled": "Email authentication is not enabled. Please ask the administrator.",
-    "failed_to_register":"Failed to register.",
-    "successfully_created":"The user {{username}} is successfully created.",
-    "can_not_activate_maximum_number_of_users":"Can not activate more than the maximum number of users.",
-    "failed_to_activate":"Failed to activate.",
-    "unable_to_use_this_user":"Unable to use this user.",
-    "complete_to_install1":"Complete to Install GROWI ! Please login as admin account.",
-    "complete_to_install2":"Complete to Install GROWI ! Please check each settings on this page first.",
-    "failed_to_create_admin_user":"Failed to create admin user. {{errMessage}}",
-    "successfully_send_email_auth":"We sent an email to {{email}}. Please click the URL in the email and complete the registration.",
+    "failed_to_register": "Failed to register.",
+    "successfully_created": "The user {{username}} is successfully created.",
+    "can_not_activate_maximum_number_of_users": "Can not activate more than the maximum number of users.",
+    "failed_to_activate": "Failed to activate.",
+    "unable_to_use_this_user": "Unable to use this user.",
+    "complete_to_install1": "Complete to Install GROWI ! Please login as admin account.",
+    "complete_to_install2": "Complete to Install GROWI ! Please check each settings on this page first.",
+    "failed_to_create_admin_user": "Failed to create admin user. {{errMessage}}",
+    "successfully_send_email_auth": "We sent an email to {{email}}. Please click the URL in the email and complete the registration.",
     "incorrect_token_or_expired_url": "The token is incorrect or the URL has expired.",
     "user_already_logged_in": "You cannot create a new account when you are logged in.",
     "registration_closed": "You are not authorized to create a new account.",
@@ -648,20 +647,20 @@
     "user_not_found": "User not found.",
     "provider_duplicated_username_exception": "<p><strong><span class='material-symbols-outlined me-1'>cancel</span>DuplicatedUsernameException occured</strong></p><p class='mb-0'> Your {{ failedProviderForDuplicatedUsernameException }} authentication was succeeded, but a new user could not be created. See the issue <a href='https://github.com/weseek/growi/issues/193'>#193</a>.</p>"
   },
-  "grid_edit":{
-    "create_bootstrap_4_grid":"Create Bootstrap 4 Grid",
+  "grid_edit": {
+    "create_bootstrap_4_grid": "Create Bootstrap 4 Grid",
     "grid_settings": "Grid Settings",
-    "grid_pattern":"Grid Pattern",
-    "division":"Divisions",
-    "smart_no":"Smartphone / No Break",
-    "break_point":"Break point by display size"
+    "grid_pattern": "Grid Pattern",
+    "division": "Divisions",
+    "smart_no": "Smartphone / No Break",
+    "break_point": "Break point by display size"
   },
-  "validation":{
+  "validation": {
     "aws_region": "For the region, enter the AWS region name. ex):us-east-1",
-    "aws_custom_endpoint":"For the custom endpoint, specify the URL that starts with http(s)://. Also, the trailing slash is not required.",
-    "failed_to_send_a_test_email":"Failed to send a test email using SMTP. Please check your settings."
+    "aws_custom_endpoint": "For the custom endpoint, specify the URL that starts with http(s)://. Also, the trailing slash is not required.",
+    "failed_to_send_a_test_email": "Failed to send a test email using SMTP. Please check your settings."
   },
-  "forgot_password":{
+  "forgot_password": {
     "forgot_password": "Forgot Password?",
     "send": "Send",
     "return_to_login": "Return to login",
@@ -678,7 +677,7 @@
     "password_and_confirm_password_does_not_match": "Password and confirm password does not match",
     "please_enable_mailer_alert": "The password reset feature is disabled because email setup has not been completed. Please ask administrator to complete the email setup."
   },
-  "emoji" :{
+  "emoji": {
     "title": "Pick an Emoji",
     "search": "Search",
     "clear": "Clear",
@@ -708,7 +707,7 @@
       "6": "Dark Skin Tone"
     }
   },
-  "maintenance_mode":{
+  "maintenance_mode": {
     "maintenance_mode": "Maintenance Mode",
     "growi_is_under_maintenance": "GROWI is under maintenance. Please wait until it ends.",
     "admin_page": "Admin Page",
@@ -720,10 +719,10 @@
     "you_cannot_move_this_page_now": "You cannot move this page now",
     "something_went_wrong_with_moving_page": "Something went wrong with moving page"
   },
-  "duplicated_page_alert" : {
+  "duplicated_page_alert": {
     "same_page_name_exists": "Same page name exits as「{{pageName}}」",
-    "same_page_name_exists_at_path" : "Same page name as {{pageName}} exists at {{path}} ",
-    "select_page_to_see" : "Select a page to see"
+    "same_page_name_exists_at_path": "Same page name as {{pageName}} exists at {{path}} ",
+    "select_page_to_see": "Select a page to see"
   },
   "user_group": {
     "select_group": "Select group",
@@ -770,15 +769,15 @@
       }
     }
   },
-  "page_operation":{
+  "page_operation": {
     "paths_recovered": "Paths recovered successfully",
-    "path_recovery_failed":"Path recovery failed"
+    "path_recovery_failed": "Path recovery failed"
   },
   "footer": {
     "bookmarks": "Bookmarks",
     "recently_created": "Recently Created"
   },
-  "bookmark_folder":{
+  "bookmark_folder": {
     "bookmark_folder": "bookmark folder",
     "bookmark": "bookmark",
     "delete_modal": {
@@ -796,7 +795,7 @@
     "root": "root (default)"
   },
   "v5_page_migration": {
-    "page_tree_not_avaliable" : "Page tree feature is not available yet.",
+    "page_tree_not_avaliable": "Page tree feature is not available yet.",
     "go_to_settings": "Go to settings to enable the feature"
   },
   "questionnaire": {
@@ -842,6 +841,8 @@
     "fail_publish_page": "Failed to publish the Page"
   },
   "sidebar_header": {
-    "show_wip_page": "Show WIP"
+    "show_wip_page": "Show WIP",
+    "size_s": "Size: S",
+    "size_l": "Size: L"
   }
 }

+ 64 - 63
apps/app/public/static/locales/ja_JP/translation.json

@@ -10,10 +10,10 @@
   "Duplicate": "複製",
   "PathRecovery": "パスを修復",
   "Copy": "コピー",
-  "preview":"プレビュー",
-  "desktop":"パソコン",
-  "phone":"スマホ",
-  "tablet":"タブレット",
+  "preview": "プレビュー",
+  "desktop": "パソコン",
+  "phone": "スマホ",
+  "tablet": "タブレット",
   "Click to copy": "クリックでコピー",
   "Rename": "名前変更",
   "Move/Rename": "移動/名前変更",
@@ -146,9 +146,9 @@
   "Recent Changes": "最新の変更",
   "Page Tree": "ページツリー",
   "In-App Notification": "通知",
-  "original_path":"元のパス",
-  "new_path":"新しいパス",
-  "duplicated_path":"重複したパス",
+  "original_path": "元のパス",
+  "new_path": "新しいパス",
+  "duplicated_path": "重複したパス",
   "Link sharing is disabled": "リンクのシェアは無効化されています",
   "successfully_saved_the_page": "ページが正常に保存されました",
   "you_can_not_create_page_with_this_name": "この名前でページを作成することはできません",
@@ -213,7 +213,7 @@
   },
   "Password": "パスワード",
   "Password Settings": "パスワード設定",
-  "personal_settings":{
+  "personal_settings": {
     "disassociate_external_account": "External Account の連携解除",
     "disassociate_external_account_desc": "<strong>{{providerType}}</strong> プロバイダーの <strong>{{accountId}}</strong> アカウントを連携解除します",
     "set_new_password": "パスワードを新規に設定",
@@ -227,7 +227,7 @@
     "Shere this page link to public": "外部に共有するリンクを発行する",
     "share_link_list": "共有リンクリスト",
     "share_link_management": "共有リンク管理",
-    "delete_all_share_links":"全ての共有リンクを削除します",
+    "delete_all_share_links": "全ての共有リンクを削除します",
     "expire": "有効期限",
     "Days": "日間",
     "Custom": "カスタム",
@@ -235,8 +235,8 @@
     "enter_desc": "概要を入力",
     "Unlimited": "無期限",
     "Issue": "発行",
-    "share_settings" :"共有設定",
-    "Invalid_Number_of_Date" : "有効期限の日数には整数を入力してください",
+    "share_settings": "共有設定",
+    "Invalid_Number_of_Date": "有効期限の日数には整数を入力してください",
     "link_sharing_is_disabled": "リンクのシェアは無効化されています"
   },
   "API Settings": "API設定",
@@ -281,7 +281,7 @@
       "no_zero_width_spaces": "ゼロ幅スペースを許可しません。",
       "period_in_list_item": "リストアイテムのピリオドの有無をチェックします。",
       "use_si_units": "SI単位系以外の使用を禁止します。"
-      },
+    },
     "japanese_settings": {
       "japanese_settings": "日本語設定",
       "ja_hiragana_keishikimeishi": "漢字よりひらがなで書かれた読みやすい形式名詞をチェックします。",
@@ -335,7 +335,7 @@
     "notice": {
       "version": "これは最新のバージョンではありません。",
       "redirected": "リダイレクト元 >>",
-      "redirected_period":"",
+      "redirected_period": "",
       "unlinked": "このページへのリダイレクトは削除されました。",
       "restricted": "このページの閲覧は制限されています",
       "stale": "このページは最終更新日から{{count}}年以上が経過しています。",
@@ -375,8 +375,8 @@
     "comparing_source": "ソース",
     "comparing_target": "ターゲット",
     "comparing_revisions": "差分を比較する",
-    "compare_latest":"最新と比較",
-    "compare_previous":"1つ前のバージョンと比較"
+    "compare_latest": "最新と比較",
+    "compare_previous": "1つ前のバージョンと比較"
   },
   "modal_rename": {
     "label": {
@@ -414,7 +414,7 @@
   "deleted_pages_completely": "{{path}} を完全に削除しました",
   "renamed_pages": "{{path}} を移動/名前変更しました",
   "empty_trash": "ゴミ箱を空にしました",
-  "modal_empty":{
+  "modal_empty": {
     "empty_the_trash": "ゴミ箱を空にする",
     "empty_the_trash_button": "空にする",
     "not_deletable_notice": "権限がないため、いくつかのページは削除できません",
@@ -474,10 +474,9 @@
     "file_conflicting_with_newer_remote": "サーバー側の新しいファイルと衝突します。",
     "resolve_conflict_message": "ページ本文を選んでください",
     "resolve_conflict": "衝突を解消",
-    "resolve_and_save" : "解消し保存する",
-    "select_revision" : "{{revision}}にする",
+    "resolve_and_save": "解消し保存する",
+    "select_revision": "{{revision}}にする",
     "requested_revision": "送信された本文",
-    "origin_revision": "送信する前の本文",
     "latest_revision": "最新の本文",
     "selected_editable_revision": "保存するページ本文(編集可能)"
   },
@@ -504,7 +503,7 @@
     "issue_share_link": "共有リンクを作成しました",
     "remove_share_link": "共有リンクを{{count}}件削除しました",
     "switch_disable_link_sharing_success": "共有リンクの設定を変更しました",
-    "failed_to_reset_password":"パスワードのリセットに失敗しました",
+    "failed_to_reset_password": "パスワードのリセットに失敗しました",
     "save_succeeded": "保存に成功しました"
   },
   "template": {
@@ -571,12 +570,12 @@
     "deletion_modal_header": "以下のページを削除",
     "delete_completely": "完全に削除する",
     "include_certain_path": "{{pathToInclude}}下を含む ",
-    "delete_all_selected_page" : "一括削除",
-    "currently_not_implemented":"現在未実装の機能です",
-    "search_again" : "再検索",
-    "number_of_list_to_display" : "表示件数",
-    "page_number_unit" : "件",
-    "hit_number_unit" : "件",
+    "delete_all_selected_page": "一括削除",
+    "currently_not_implemented": "現在未実装の機能です",
+    "search_again": "再検索",
+    "number_of_list_to_display": "表示件数",
+    "page_number_unit": "件",
+    "hit_number_unit": "件",
     "sort_axis": {
       "relationScore": "関連度順",
       "createdAt": "作成日時",
@@ -623,7 +622,7 @@
     "sign_in_error": "ログインエラー",
     "registration_successful": "登録が完了しました。管理者の承認をお待ちください。",
     "Setup": "セットアップ",
-    "enabled_ldap_has_configuration_problem":"LDAPは有効ですが、設定に問題があります。",
+    "enabled_ldap_has_configuration_problem": "LDAPは有効ですが、設定に問題があります。",
     "set_env_var_for_logs": "(ログを取得するためには、環境変数 <code>DEBUG=crowi:service:PassportService</code> を設定してください。)"
   },
   "invited": {
@@ -649,23 +648,23 @@
     "sign_in_failure": "ログインに失敗しました。",
     "aws_sttings_required": "この機能にはAWS設定が必要です。管理者に訪ねて下さい。",
     "application_already_installed": "アプリケーションのインストールが完了しました。",
-    "email_address_could_not_be_used":"このメールアドレスは使用できません。(許可されたメールアドレスを確認してください。)",
-    "user_id_is_not_available":"このユーザーIDは使用できません。",
-    "username_should_not_be_null":"Username が null になっています 管理画面の認証機構設定にて設定の確認をしてください",
-    "email_address_is_already_registered":"このメールアドレスは既に登録されています。",
-    "can_not_register_maximum_number_of_users":"ユーザー数が上限を超えたため登録できません。",
-    "email_settings_is_not_setup":"E-mail 設定が完了していません。管理者に問い合わせてください。",
+    "email_address_could_not_be_used": "このメールアドレスは使用できません。(許可されたメールアドレスを確認してください。)",
+    "user_id_is_not_available": "このユーザーIDは使用できません。",
+    "username_should_not_be_null": "Username が null になっています 管理画面の認証機構設定にて設定の確認をしてください",
+    "email_address_is_already_registered": "このメールアドレスは既に登録されています。",
+    "can_not_register_maximum_number_of_users": "ユーザー数が上限を超えたため登録できません。",
+    "email_settings_is_not_setup": "E-mail 設定が完了していません。管理者に問い合わせてください。",
     "email_authentication_is_not_enabled": "メール認証が有効になっていません。管理者に問い合わせてください。",
-    "failed_to_register":"登録に失敗しました。",
-    "successfully_created":"{{username}} が作成されました。",
-    "can_not_activate_maximum_number_of_users":"ユーザーが上限に達したためアクティベートできません。",
-    "failed_to_activate":"アクティベートに失敗しました。",
-    "unable_to_use_this_user":"利用できないユーザーIDです。",
-    "complete_to_install1":"GROWI のインストールが完了しました!管理者アカウントでログインしてください。",
-    "complete_to_install2":"GROWI のインストールが完了しました!はじめに、このページで各種設定を確認してください。",
-    "failed_to_create_admin_user":"管理ユーザーの作成に失敗しました。{{errMessage}}",
-    "successfully_send_email_auth":"{{email}} にメールを送信しました。添付されたURLをクリックし、本登録を完了させてください",
-    "incorrect_token_or_expired_url":"トークンが正しくないか、URLの有効期限が切れています。",
+    "failed_to_register": "登録に失敗しました。",
+    "successfully_created": "{{username}} が作成されました。",
+    "can_not_activate_maximum_number_of_users": "ユーザーが上限に達したためアクティベートできません。",
+    "failed_to_activate": "アクティベートに失敗しました。",
+    "unable_to_use_this_user": "利用できないユーザーIDです。",
+    "complete_to_install1": "GROWI のインストールが完了しました!管理者アカウントでログインしてください。",
+    "complete_to_install2": "GROWI のインストールが完了しました!はじめに、このページで各種設定を確認してください。",
+    "failed_to_create_admin_user": "管理ユーザーの作成に失敗しました。{{errMessage}}",
+    "successfully_send_email_auth": "{{email}} にメールを送信しました。添付されたURLをクリックし、本登録を完了させてください",
+    "incorrect_token_or_expired_url": "トークンが正しくないか、URLの有効期限が切れています。",
     "user_already_logged_in": "ログイン中のため、新規アカウントを作成できませんでした。",
     "registration_closed": "新しいアカウントを作成する権限がありません。",
     "Username has invalid characters": "ユーザー名に不正な文字が含まれています.",
@@ -681,20 +680,20 @@
     "user_not_found": "ユーザーが見つかりません",
     "provider_duplicated_username_exception": "<p><strong><span class='material-symbols-outlined me-1'>cancel</span>エラー: DuplicatedUsernameException</strong></p><p class='mb-0'> {{ failedProviderForDuplicatedUsernameException }} 認証は成功しましたが、新しいユーザーを作成できませんでした。詳しくは<a href='https://github.com/weseek/growi/issues/193'>こちら: #193</a>.</p>"
   },
-  "grid_edit":{
-    "create_bootstrap_4_grid":"Bootstrap 4 グリッドを作成",
+  "grid_edit": {
+    "create_bootstrap_4_grid": "Bootstrap 4 グリッドを作成",
     "grid_settings": "グリッド設定",
-    "grid_pattern":"グリッド パターン",
-    "division":"分割",
-    "smart_no":"スマホ / 分割なし",
-    "break_point":"画面サイズより分割"
+    "grid_pattern": "グリッド パターン",
+    "division": "分割",
+    "smart_no": "スマホ / 分割なし",
+    "break_point": "画面サイズより分割"
   },
-  "validation":{
+  "validation": {
     "aws_region": "リージョンには、AWSリージョン名を入力してください。例: ap-northeast-1",
     "aws_custom_endpoint": "カスタムエンドポイントは、http(s)://で始まるURLを指定してください。また、末尾の/は不要です。",
-    "failed_to_send_a_test_email":"SMTPを利用したテストメール送信に失敗しました。設定をみなおしてください。"
+    "failed_to_send_a_test_email": "SMTPを利用したテストメール送信に失敗しました。設定をみなおしてください。"
   },
-  "forgot_password":{
+  "forgot_password": {
     "forgot_password": "パスワードをお忘れですか?",
     "send": "送信",
     "return_to_login": "ログイン画面に戻る",
@@ -707,11 +706,11 @@
     "email_is_required": "メールを入力してください",
     "success_to_send_email": "メールを送信しました",
     "feature_is_unavailable": "この機能を利用することはできません。",
-    "incorrect_token_or_expired_url":"トークンが正しくないか、URLの有効期限が切れています。 以下のリンクからパスワードリセットリクエストを再送信してください。",
+    "incorrect_token_or_expired_url": "トークンが正しくないか、URLの有効期限が切れています。 以下のリンクからパスワードリセットリクエストを再送信してください。",
     "password_and_confirm_password_does_not_match": "パスワードと確認パスワードが一致しません",
     "please_enable_mailer_alert": "メール設定が完了していないため、パスワード再設定機能が無効になっています。メール設定を完了させるよう管理者に依頼してください。"
   },
-  "emoji" :{
+  "emoji": {
     "title": "絵文字を選択",
     "search": "探す",
     "clear": "リセット",
@@ -741,7 +740,7 @@
       "6": "肌の色が濃い"
     }
   },
-  "maintenance_mode":{
+  "maintenance_mode": {
     "maintenance_mode": "メンテナンスモード",
     "growi_is_under_maintenance": "GROWI はメンテナンス中です。終了するまでお待ちください",
     "admin_page": "管理画面へ",
@@ -753,10 +752,10 @@
     "you_cannot_move_this_page_now": "現在、このページを移動することはできません",
     "something_went_wrong_with_moving_page": "ページの移動に問題が発生しました"
   },
-  "duplicated_page_alert" : {
+  "duplicated_page_alert": {
     "same_page_name_exists": "ページ名 「{{pageName}}」が重複しています",
-    "same_page_name_exists_at_path" : "”{{path}}” において ”{{pageName}}”というページは複数存在しています。",
-    "select_page_to_see" : "以下から遷移するページを選択してください。"
+    "same_page_name_exists_at_path": "”{{path}}” において ”{{pageName}}”というページは複数存在しています。",
+    "select_page_to_see": "以下から遷移するページを選択してください。"
   },
   "user_group": {
     "select_group": "グループを選ぶ",
@@ -803,15 +802,15 @@
       }
     }
   },
-  "page_operation":{
+  "page_operation": {
     "paths_recovered": "パスを修復しました",
-    "path_recovery_failed":"パスを修復できませんでした"
+    "path_recovery_failed": "パスを修復できませんでした"
   },
   "footer": {
     "bookmarks": "ブックマーク",
     "recently_created": "最近作成したページ"
   },
-  "bookmark_folder":{
+  "bookmark_folder": {
     "bookmark_folder": "ブックマークフォルダ",
     "bookmark": "ブックマーク",
     "delete_modal": {
@@ -829,7 +828,7 @@
     "root": "root (default)"
   },
   "v5_page_migration": {
-    "page_tree_not_avaliable" : "Page Tree 機能は現在使用できません。",
+    "page_tree_not_avaliable": "Page Tree 機能は現在使用できません。",
     "go_to_settings": "設定する"
   },
   "questionnaire": {
@@ -875,6 +874,8 @@
     "fail_publish_page": "WIP を解除できませんでした"
   },
   "sidebar_header": {
-    "show_wip_page": "WIP を表示"
+    "show_wip_page": "WIP を表示",
+    "size_s": "サイズ: S",
+    "size_l": "サイズ: L"
   }
 }

+ 367 - 366
apps/app/public/static/locales/zh_CN/translation.json

@@ -4,59 +4,59 @@
   },
   "Help": "帮助",
   "view": "View",
-	"Edit": "编辑",
-	"Delete": "删除",
-	"delete_all": "删除所有",
-	"Duplicate": "复制",
+  "Edit": "编辑",
+  "Delete": "删除",
+  "delete_all": "删除所有",
+  "Duplicate": "复制",
   "PathRecovery": "路径恢复",
-	"Copy": "复制",
-  "preview":"预览",
-  "desktop":"电脑",
-  "phone":"手机",
-  "tablet":"平板",
-	"Login": "登录",
-	"Click to copy": "点击复制",
+  "Copy": "复制",
+  "preview": "预览",
+  "desktop": "电脑",
+  "phone": "手机",
+  "tablet": "平板",
+  "Login": "登录",
+  "Click to copy": "点击复制",
   "Rename": "重命名",
-	"Move/Rename": "移动/重命名",
-	"Redirected": "重定向",
-	"Unlinked": "Unlinked",
+  "Move/Rename": "移动/重命名",
+  "Redirected": "重定向",
+  "Unlinked": "Unlinked",
   "unlink_redirection": "取消链接重定向",
   "Done": "Done",
   "Cancel": "取消",
-	"Create": "创建",
+  "Create": "创建",
   "Description": "描述",
-	"Admin": "管理",
-	"administrator": "管理员",
-	"Tags": "Tags",
+  "Admin": "管理",
+  "administrator": "管理员",
+  "Tags": "Tags",
   "Close": "Close",
-	"Shortcuts": "快捷方式",
+  "Shortcuts": "快捷方式",
   "CustomSidebar": "Custom Sidebar",
-	"eg": "e.g.",
-	"add": "添加",
-	"Undo": "撤销",
-	"account_id": "用户Id",
-	"Initialize": "初始化",
+  "eg": "e.g.",
+  "add": "添加",
+  "Undo": "撤销",
+  "account_id": "用户Id",
+  "Initialize": "初始化",
   "Update": "更新",
-	"Update Page": "更新本页",
-	"Error": "误差",
-	"Warning": "警告",
+  "Update Page": "更新本页",
+  "Error": "误差",
+  "Warning": "警告",
   "Sign in": "登录",
-	"Sign up is here": "注册",
-	"Sign in is here": "登录",
-	"Sign up": "注册",
-	"Sign up with Google Account": "Sign up with Google Account",
-	"Sign in with Google Account": "Sign in with Google Account",
-	"Sign up with this Google Account": "Sign up with this Google Account",
-	"Example": "例如",
-	"Taro Yamada": "John Doe",
+  "Sign up is here": "注册",
+  "Sign in is here": "登录",
+  "Sign up": "注册",
+  "Sign up with Google Account": "Sign up with Google Account",
+  "Sign in with Google Account": "Sign in with Google Account",
+  "Sign up with this Google Account": "Sign up with this Google Account",
+  "Example": "例如",
+  "Taro Yamada": "John Doe",
   "Select": "请选择",
   "Required": "必需的",
-	"List View": "列表",
-	"Timeline View": "时间线",
+  "List View": "列表",
+  "Timeline View": "时间线",
   "History": "历史",
   "attachment_data": "Attachment Data",
   "No_attachments_yet": "暂无附件",
-	"Presentation Mode": "演示文稿",
+  "Presentation Mode": "演示文稿",
   "Not available for guest": "不提供给客人",
   "Not available in this version": "此版本中不提供",
   "No users have liked this yet": "还没有用户喜欢这个",
@@ -73,48 +73,48 @@
   "Specify Hierarchy": "指定层级",
   "Submitted the request to create the archive": "提交创建归档请求",
   "username": "用户名",
-	"Created": "创建",
-	"Last updated": "上次更新",
-	"Share": "分享",
+  "Created": "创建",
+  "Last updated": "上次更新",
+  "Share": "分享",
   "Share Link": "分享链接",
-	"Markdown Link": "Markdown链接",
-	"Create/Edit Template": "创建/编辑 模板页面",
-	"Unportalize": "未启动",
-	"Go to this version": "查看此版本",
-	"View diff": "查看差异",
-	"No diff": "无差异",
-	"User ID": "用户ID",
-	"Home": "首页",
-	"My Drafts": "My Drafts",
-	"User Settings": "用户设置",
-	"User Information": "用户信息",
+  "Markdown Link": "Markdown链接",
+  "Create/Edit Template": "创建/编辑 模板页面",
+  "Unportalize": "未启动",
+  "Go to this version": "查看此版本",
+  "View diff": "查看差异",
+  "No diff": "无差异",
+  "User ID": "用户ID",
+  "Home": "首页",
+  "My Drafts": "My Drafts",
+  "User Settings": "用户设置",
+  "User Information": "用户信息",
   "User Activation": "用户激活",
-	"Basic Info": "基础信息",
-	"Name": "姓名",
-	"Email": "邮箱",
-	"Language": "语言",
-	"English": "英语",
-	"Japanese": "日语",
-	"Chinese": "简体中文",
-	"Set Profile Image": "头像",
-	"Upload Image": "上传图片",
-	"Current Image": "当前图片",
-	"Delete Image": "删除图片",
-	"Delete this image?": "删除图片?",
-	"Updated": "更新",
-	"Upload new image": "上传新图像",
-	"Connected": "Connected",
-	"Show": "显示",
-	"Hide": "隐藏",
+  "Basic Info": "基础信息",
+  "Name": "姓名",
+  "Email": "邮箱",
+  "Language": "语言",
+  "English": "英语",
+  "Japanese": "日语",
+  "Chinese": "简体中文",
+  "Set Profile Image": "头像",
+  "Upload Image": "上传图片",
+  "Current Image": "当前图片",
+  "Delete Image": "删除图片",
+  "Delete this image?": "删除图片?",
+  "Updated": "更新",
+  "Upload new image": "上传新图像",
+  "Connected": "Connected",
+  "Show": "显示",
+  "Hide": "隐藏",
   "Loading": "加载...",
-	"Reset": "重置",
-	"Disclose E-mail": "显示邮箱",
-	"page exists": "页面已存在",
-	"Error occurred": "Error occurred",
-	"Input page name": "Input page name",
-	"Input page name (optional)": "Input page name (optional)",
-	"New Page": "新页面",
-	"Create under": "Create page under below:",
+  "Reset": "重置",
+  "Disclose E-mail": "显示邮箱",
+  "page exists": "页面已存在",
+  "Error occurred": "Error occurred",
+  "Input page name": "Input page name",
+  "Input page name (optional)": "Input page name (optional)",
+  "New Page": "新页面",
+  "Create under": "Create page under below:",
   "V5 Page Migration": "转换为V5的兼容性",
   "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> <span className='growi-custom-icons'>external_link</span> ",
@@ -122,27 +122,27 @@
 	"external_account_management": "外部账户管理",
   "UserGroup": "用户组",
   "ChildUserGroup": "儿童用户组",
-	"Basic Settings": "基础设置",
-	"The contents entered here will be shown in the header etc": "此处输入的内容将显示在标题等中",
-	"Public": "公共",
-	"Anyone with the link": "任何人",
-	"Specified users only": "仅指定用户",
-	"Only me": "只有我",
+  "Basic Settings": "基础设置",
+  "The contents entered here will be shown in the header etc": "此处输入的内容将显示在标题等中",
+  "Public": "公共",
+  "Anyone with the link": "任何人",
+  "Specified users only": "仅指定用户",
+  "Only me": "只有我",
   "Only inside the group": "仅组内",
   "page_list": "Page List",
-	"Reselect the group": "重新选择组",
-	"Shareable link": "可分享链接",
-	"The whitelist of registration permission E-mail address": "注册许可电子邮件地址的白名单",
-	"Add tags for this page": "添加标签",
+  "Reselect the group": "重新选择组",
+  "Shareable link": "可分享链接",
+  "The whitelist of registration permission E-mail address": "注册许可电子邮件地址的白名单",
+  "Add tags for this page": "添加标签",
   "tag_list": "标签列表",
   "popular_tags": "流行标签",
   "Check All tags": "检查所有标签",
-	"You have no tag, You can set tags on pages": "你没有标签,可以在页面上设置标签",
-	"Show latest": "显示最新",
-	"Load latest": "家在最新",
-	"edited this page": "edited this page.",
-	"List Drafts": "草稿",
-	"Deleted Pages": "已删除页",
+  "You have no tag, You can set tags on pages": "你没有标签,可以在页面上设置标签",
+  "Show latest": "显示最新",
+  "Load latest": "家在最新",
+  "edited this page": "edited this page.",
+  "List Drafts": "草稿",
+  "Deleted Pages": "已删除页",
   "Disassociate": "解除关联",
   "No bookmarks yet": "暂无书签",
   "add_bookmark": "添加到书签",
@@ -151,9 +151,9 @@
   "Recent Changes": "最新修改",
   "Page Tree": "页面树",
   "In-App Notification": "通知",
-  "original_path":"Original path",
-  "new_path":"New path",
-  "duplicated_path":"Duplicated path",
+  "original_path": "Original path",
+  "new_path": "New path",
+  "duplicated_path": "Duplicated path",
   "Link sharing is disabled": "你不允许分享该链接",
   "successfully_saved_the_page": "成功地保存了该页面",
   "you_can_not_create_page_with_this_name": "您无法使用此名称创建页面",
@@ -161,10 +161,10 @@
   "Confirm": "确定",
   "Successfully requested": "进程成功接受",
   "copied_to_clipboard": "它已复制到剪贴板。",
-	"form_validation": {
-		"error_message": "有些值不正确",
-		"required": "%s 是必需的",
-		"invalid_syntax": "%s的语法无效。",
+  "form_validation": {
+    "error_message": "有些值不正确",
+    "required": "%s 是必需的",
+    "invalid_syntax": "%s的语法无效。",
     "title_required": "标题是必需的。",
     "field_required": "{{target}} 是必需的"
   },
@@ -177,63 +177,63 @@
   "custom_navigation": {
     "no_pages_under_this_page": "There are no pages under this page."
   },
-	"installer": {
+  "installer": {
     "tab": "创建账户",
     "title": "安装",
-		"setup": "安装",
-		"create_initial_account": "创建初始用户",
-		"initial_account_will_be_administrator_automatically": "初始帐户将自动成为管理员。",
-		"unavaliable_user_id": "用户ID不可用",
+    "setup": "安装",
+    "create_initial_account": "创建初始用户",
+    "initial_account_will_be_administrator_automatically": "初始帐户将自动成为管理员。",
+    "unavaliable_user_id": "用户ID不可用",
     "failed_to_install": "GROWI安装失败。请再试一次。",
     "failed_to_login_after_install": "安装后登录失败。重定向到登录表格..."
-	},
-	"breaking_changes": {
-		"v346_using_basic_auth": "当前使用的基本身份验证在不久的将来将不再可用。从%s中删除设置"
-	},
-	"page_register": {
+  },
+  "breaking_changes": {
+    "v346_using_basic_auth": "当前使用的基本身份验证在不久的将来将不再可用。从%s中删除设置"
+  },
+  "page_register": {
     "send_email": "发电子邮件",
-		"notice": {
-			"restricted": "需要管理员批准。",
-			"restricted_defail": "一旦管理员批准您的注册,您就可以访问此wiki。"
-		},
-		"form_help": {
-			"email": "您必须有下面列出的电子邮件地址才能注册此wiki。",
-			"password": "密码长度必须至少为8个字符。",
-			"user_id": "您创建的网页的URL将包含您的用户ID。您的用户ID可以由字母、数字和一些符号组成。"
-		}
-	},
-	"Settings": "设置",
-	"page_me": {
-		"form_help": {
-			"profile_image1": "图像上传设置未完成。",
-			"profile_image2": "设置AWS或启用本地上传。"
-		}
-	},
-	"page_me_apitoken": {
+    "notice": {
+      "restricted": "需要管理员批准。",
+      "restricted_defail": "一旦管理员批准您的注册,您就可以访问此wiki。"
+    },
+    "form_help": {
+      "email": "您必须有下面列出的电子邮件地址才能注册此wiki。",
+      "password": "密码长度必须至少为8个字符。",
+      "user_id": "您创建的网页的URL将包含您的用户ID。您的用户ID可以由字母、数字和一些符号组成。"
+    }
+  },
+  "Settings": "设置",
+  "page_me": {
+    "form_help": {
+      "profile_image1": "图像上传设置未完成。",
+      "profile_image2": "设置AWS或启用本地上传。"
+    }
+  },
+  "page_me_apitoken": {
     "api_token": "API Token",
-		"notice": {
-			"apitoken_issued": "API token 未发布。",
-			"update_token1": "您可以更新以生成新的API令牌。",
-			"update_token2": "您需要更新任何现有进程中的API令牌。"
-		}
-	},
-	"Password": "密码",
-	"Password Settings": "密码设置",
-	"personal_settings": {
-		"disassociate_external_account": "解除与外部帐户的关联",
-		"disassociate_external_account_desc": "是否确实要解除与<strong>{{providerType}}</strong>帐户<strong>{{providerType}}</strong> 的关联?",
-		"set_new_password": "设置新密码",
-		"update_password": "更新密码",
-		"current_password": "当前密码",
-		"new_password": "新密码",
-		"new_password_confirm": "重复新密码",
-		"password_is_not_set": "密码未设置"
-	},
-	"API Settings": "API设置",
+    "notice": {
+      "apitoken_issued": "API token 未发布。",
+      "update_token1": "您可以更新以生成新的API令牌。",
+      "update_token2": "您需要更新任何现有进程中的API令牌。"
+    }
+  },
+  "Password": "密码",
+  "Password Settings": "密码设置",
+  "personal_settings": {
+    "disassociate_external_account": "解除与外部帐户的关联",
+    "disassociate_external_account_desc": "是否确实要解除与<strong>{{providerType}}</strong>帐户<strong>{{providerType}}</strong> 的关联?",
+    "set_new_password": "设置新密码",
+    "update_password": "更新密码",
+    "current_password": "当前密码",
+    "new_password": "新密码",
+    "new_password_confirm": "重复新密码",
+    "password_is_not_set": "密码未设置"
+  },
+  "API Settings": "API设置",
   "Other Settings": "其他设置",
-	"API Token Settings": "API token 设置",
-	"Current API Token": "当前 API token",
-	"Update API Token": "更新 API token",
+  "API Token Settings": "API token 设置",
+  "Current API Token": "当前 API token",
+  "Update API Token": "更新 API token",
   "in_app_notification_settings": {
     "in_app_notification_settings": "在应用程序通知设置",
     "subscribe_settings": "自动订阅(接收通知)页面的设置",
@@ -259,43 +259,43 @@
   "editor_settings": {
     "editor_settings": "编辑器设置"
   },
-	"search_help": {
-		"title": "搜索帮助",
-		"and": {
-			"syntax help": "用空格分隔",
-			"desc": "在标题或正文中同时包含{{word1}、{{word2}的搜索页"
-		},
-		"exclude": {
-			"desc": "排除标题或正文中包含{{word}的页"
-		},
-		"phrase": {
-			"syntax help": "用双引号括起来",
-			"desc": "包含短语“{{phrase}”的搜索页"
-		},
-		"prefix": {
-			"desc": "只搜索标题以{{path}开头的页"
-		},
-		"exclude_prefix": {
-			"desc": "排除标题以{{path}开头的页"
-		},
-		"tag": {
-			"desc": "搜索带有{{tag}标记的页面"
-		},
-		"exclude_tag": {
-			"desc": "排除带有{{tag}标记的页"
-		}
-	},
-	"search": {
-		"search page bodies": "按[回车]键进行全文搜索"
-	},
-	"page_page": {
-		"notice": {
-			"version": "这不是当前版本。",
-			"redirected": "您将从",
+  "search_help": {
+    "title": "搜索帮助",
+    "and": {
+      "syntax help": "用空格分隔",
+      "desc": "在标题或正文中同时包含{{word1}、{{word2}的搜索页"
+    },
+    "exclude": {
+      "desc": "排除标题或正文中包含{{word}的页"
+    },
+    "phrase": {
+      "syntax help": "用双引号括起来",
+      "desc": "包含短语“{{phrase}”的搜索页"
+    },
+    "prefix": {
+      "desc": "只搜索标题以{{path}开头的页"
+    },
+    "exclude_prefix": {
+      "desc": "排除标题以{{path}开头的页"
+    },
+    "tag": {
+      "desc": "搜索带有{{tag}标记的页面"
+    },
+    "exclude_tag": {
+      "desc": "排除带有{{tag}标记的页"
+    }
+  },
+  "search": {
+    "search page bodies": "按[回车]键进行全文搜索"
+  },
+  "page_page": {
+    "notice": {
+      "version": "这不是当前版本。",
+      "redirected": "您将从",
       "redirected_period": "",
-			"unlinked": "将网页重定向到此网页已被删除。",
-			"restricted": "访问此页受到限制",
-			"stale": "自上次更新以来,已超过{{count}年。",
+      "unlinked": "将网页重定向到此网页已被删除。",
+      "restricted": "访问此页受到限制",
+      "stale": "自上次更新以来,已超过{{count}年。",
       "stale_plural": "自上次更新以来已过去{{count}年以上。",
       "no_deadline": "This page has no expiration date"
 		}
@@ -318,11 +318,11 @@
     "display_the_page_when_posting_this_comment": "Display the page when posting this comment",
     "no_user_found": "未找到用户名"
   },
-	"page_api_error": {
-		"notfound_or_forbidden": "未找到或禁止原始页。",
-		"already_exists": "具有该路径的页面已存在",
-		"outdated": "页面已被某人更新,现在已过时。",
-		"user_not_admin": "仅管理员用户可以删除",
+  "page_api_error": {
+    "notfound_or_forbidden": "未找到或禁止原始页。",
+    "already_exists": "具有该路径的页面已存在",
+    "outdated": "页面已被某人更新,现在已过时。",
+    "user_not_admin": "仅管理员用户可以删除",
     "single_deletion_empty_pages": "空的页面不能被单一删除",
     "complete_deletion_not_allowed_for_user": "您无权永久删除该页面"
   },
@@ -332,11 +332,11 @@
     "comparing_source": "源头",
     "comparing_target": "目标",
     "comparing_revisions": "比较两者的区别",
-    "compare_latest":"比較最新版本",
-    "compare_previous":"比較以前的版本"
+    "compare_latest": "比較最新版本",
+    "compare_previous": "比較以前的版本"
   },
-	"modal_rename": {
-		"label": {
+  "modal_rename": {
+    "label": {
       "Move/Rename page": "页面 移动/重命名",
       "New page name": "新建页面名称",
       "Failed to get subordinated pages": "Failed to get subordinated pages",
@@ -347,42 +347,42 @@
       "Other options": "其他选项",
       "Do not update metadata": "不更新元数据",
       "Redirect": "重定向"
-		},
-		"help": {
+    },
+    "help": {
       "redirect": "Redirect to new page if someone accesses <code>%s</code>",
       "metadata": "Remains last update user and updated date as is",
       "recursive": "Move/Rename children of under <code>%s</code> recursively"
-		}
-	},
-	"Put Back": "Put back",
+    }
+  },
+  "Put Back": "Put back",
   "Delete Completely": "Delete completely",
   "page_has_been_reverted": "{{path}} 已还原",
-	"modal_delete": {
-		"delete_page": "Delete page",
-		"deleting_page": "Deleting page",
-		"delete_recursively": "Delete child pages recursively.",
-		"delete_completely": "Delete completely",
-		"delete_completely_restriction": "You don't have the authority to delete pages completely.",
-		"recursively": "Delete children of <code>%s</code> recursively.",
-		"completely": "Delete completely instead of putting it into trash."
+  "modal_delete": {
+    "delete_page": "Delete page",
+    "deleting_page": "Deleting page",
+    "delete_recursively": "Delete child pages recursively.",
+    "delete_completely": "Delete completely",
+    "delete_completely_restriction": "You don't have the authority to delete pages completely.",
+    "recursively": "Delete children of <code>%s</code> recursively.",
+    "completely": "Delete completely instead of putting it into trash."
   },
   "deleted_page": "移到了垃圾箱。",
   "deleted_pages": "将 {{path}} 放入垃圾箱",
   "deleted_pages_completely": "{{path}} 已被完全删除",
   "renamed_pages": "移动/重命名 {{path}}",
   "empty_trash": "清空垃圾",
-	"modal_empty": {
-		"empty_the_trash": "清空垃圾",
+  "modal_empty": {
+    "empty_the_trash": "清空垃圾",
     "empty_the_trash_button": "清空垃圾",
     "not_deletable_notice": "由于缺乏权限,一些页面不能被删除",
-		"notice": "完全删除的页面是不可恢复的。"
-	},
-	"modal_duplicate": {
-		"label": {
-			"Duplicate page": "Duplicate page",
+    "notice": "完全删除的页面是不可恢复的。"
+  },
+  "modal_duplicate": {
+    "label": {
+      "Duplicate page": "Duplicate page",
       "New page name": "New page name",
       "Failed to get subordinated pages": "Failed to get subordinated pages",
-			"Current page name": "Current page name",
+      "Current page name": "Current page name",
       "Recursively": "Recursively",
       "Duplicate without exist path": "Duplicate without exist path",
       "Same page already exists": "Same page already exists",
@@ -394,46 +394,45 @@
     }
   },
   "duplicated_pages": "{{fromPath}} 已重复",
-	"modal_putback": {
-		"label": {
-			"Put Back Page": "Put back page",
-			"recursively": "Put back recursively"
-		},
-		"help": {
-			"recursively": "Put back children of under <code>%s</code> recursively"
-		}
-	},
-	"modal_shortcuts": {
-		"global": {
-			"title": "全局快捷方式",
-			"Open/Close shortcut help": "打开/关闭快捷方式帮助",
-			"Edit Page": "编辑页面",
-			"Create Page": "创建页面",
+  "modal_putback": {
+    "label": {
+      "Put Back Page": "Put back page",
+      "recursively": "Put back recursively"
+    },
+    "help": {
+      "recursively": "Put back children of under <code>%s</code> recursively"
+    }
+  },
+  "modal_shortcuts": {
+    "global": {
+      "title": "全局快捷方式",
+      "Open/Close shortcut help": "打开/关闭快捷方式帮助",
+      "Edit Page": "编辑页面",
+      "Create Page": "创建页面",
       "Search": "搜索",
-			"Show Contributors": "显示参与者",
-			"Konami Code": "Konami Code",
-			"konami_code_url": "https://en.wikipedia.org/wiki/Konami_Code"
-		},
-		"editor": {
-			"title": "编辑器快捷方式",
-			"Indent": "缩进",
-			"Outdent": "回退缩进",
-			"Save Page": "保存页面",
-			"Delete Line": "删除行"
-		},
-		"commentform": {
-			"title": "注释窗体快捷方式",
-			"Post": "提交"
-		}
-	},
+      "Show Contributors": "显示参与者",
+      "Konami Code": "Konami Code",
+      "konami_code_url": "https://en.wikipedia.org/wiki/Konami_Code"
+    },
+    "editor": {
+      "title": "编辑器快捷方式",
+      "Indent": "缩进",
+      "Outdent": "回退缩进",
+      "Save Page": "保存页面",
+      "Delete Line": "删除行"
+    },
+    "commentform": {
+      "title": "注释窗体快捷方式",
+      "Post": "提交"
+    }
+  },
   "modal_resolve_conflict": {
     "file_conflicting_with_newer_remote": "此文件与较新的远程文件冲突",
     "resolve_conflict_message": "选择页面正文",
     "resolve_conflict": "解决冲突",
-    "resolve_and_save" : "解决冲突并保存",
-    "select_revision" : "选择{{revision}}",
+    "resolve_and_save": "解决冲突并保存",
+    "select_revision": "选择{{revision}}",
     "requested_revision": "发送的页面正文",
-    "origin_revision": "发送前的页面正文",
     "latest_revision": "最新页面正文",
     "selected_editable_revision": "选定的可编辑页面正文"
   },
@@ -453,64 +452,64 @@
     "preview": "Preview",
     "page_not_found_in_preview": "\"{{path}}\" is not a GROWI page."
   },
-	"toaster": {
+  "toaster": {
     "file_upload_failed": "文件上传失败",
     "initialize_successed": "Succeeded to initialize {{target}}",
     "switch_disable_link_sharing_success": "成功更新分享链接设置",
-    "failed_to_reset_password":"Failed to reset password",
+    "failed_to_reset_password": "Failed to reset password",
     "save_succeeded": "已成功保存",
     "issue_share_link": "Succeeded to issue new share link"
   },
-	"template": {
-		"modal_label": {
+  "template": {
+    "modal_label": {
       "Select template": "选择模板",
-			"Create/Edit Template Page": "创建/编辑模板页",
-			"Create template under": "在下面创建模板页"
-		},
-		"option_label": {
-			"create/edit": "创建/编辑模板页。",
-			"select": "选择模板页面类型"
-		},
-		"children": {
-			"label": "子模板",
-			"desc": "仅应用于模板存在的同一级别页"
-		},
-		"descendants": {
-			"label": "子代模板",
-			"desc": "适用于所有分散页"
-		}
-	},
-	"sandbox": {
-		"header": "标题",
-		"header_x": "标题{{index}",
-		"block": "段落",
-		"block_detail": "写一段",
-		"empty_line": "空行",
-		"line_break": "换行符",
-		"line_break_detail": "(2空格)换行",
-		"typography": "排版",
-		"italics": "斜体",
-		"bold": "加粗",
-		"italic_bold": "斜体加粗",
-		"strikethrough": "删除线",
-		"link": "链接",
-		"code_highlight": "代码突出显示",
-		"list": "列表",
-		"unordered_list_x": "无序列表{{index}}",
-		"ordered_list_x": "有序列表{{index}}",
-		"task": "任务",
-		"task_checked": "选中的",
-		"task_unchecked": "未选中的",
-		"quote": "引用",
-		"quote1": "你可以写",
-		"quote2": "多行引用",
-		"quote_nested": "嵌套引用",
-		"table": "表格",
-		"image": "图片",
-		"alt_text": "Alt文本",
-		"insert_image": "插入图像",
-		"open_sandbox": "开放式沙箱"
-	},
+      "Create/Edit Template Page": "创建/编辑模板页",
+      "Create template under": "在下面创建模板页"
+    },
+    "option_label": {
+      "create/edit": "创建/编辑模板页。",
+      "select": "选择模板页面类型"
+    },
+    "children": {
+      "label": "子模板",
+      "desc": "仅应用于模板存在的同一级别页"
+    },
+    "descendants": {
+      "label": "子代模板",
+      "desc": "适用于所有分散页"
+    }
+  },
+  "sandbox": {
+    "header": "标题",
+    "header_x": "标题{{index}",
+    "block": "段落",
+    "block_detail": "写一段",
+    "empty_line": "空行",
+    "line_break": "换行符",
+    "line_break_detail": "(2空格)换行",
+    "typography": "排版",
+    "italics": "斜体",
+    "bold": "加粗",
+    "italic_bold": "斜体加粗",
+    "strikethrough": "删除线",
+    "link": "链接",
+    "code_highlight": "代码突出显示",
+    "list": "列表",
+    "unordered_list_x": "无序列表{{index}}",
+    "ordered_list_x": "有序列表{{index}}",
+    "task": "任务",
+    "task_checked": "选中的",
+    "task_unchecked": "未选中的",
+    "quote": "引用",
+    "quote1": "你可以写",
+    "quote2": "多行引用",
+    "quote_nested": "嵌套引用",
+    "table": "表格",
+    "image": "图片",
+    "alt_text": "Alt文本",
+    "insert_image": "插入图像",
+    "open_sandbox": "开放式沙箱"
+  },
   "slack_notification": {
     "popover_title": "Slack Notification",
     "popover_desc": "Input channel name. You can notify multiple channels by entering a comma-separated list."
@@ -519,7 +518,7 @@
     "Shere this page link to public": "Shere this page link to public",
     "share_link_list": "Share link list",
     "share_link_management": "Share Link Management",
-    "delete_all_share_links":"Delete all share links",
+    "delete_all_share_links": "Delete all share links",
     "expire": "Expiration",
     "Days": "Days",
     "Custom": "Custom",
@@ -527,37 +526,37 @@
     "enter_desc": "Enter description",
     "Unlimited": "unlimited",
     "Issue": "Issue",
-    "share_settings" :"Share settings",
-    "Invalid_Number_of_Date" : "You entered invalid value",
+    "share_settings": "Share settings",
+    "Invalid_Number_of_Date": "You entered invalid value",
     "link_sharing_is_disabled": "链接共享已被禁用"
   },
-	"search_result": {
+  "search_result": {
     "title": "搜索",
-		"result_meta": "搜索结果:",
-		"deletion_mode_btn_lavel": "选择并删除页面",
-		"cancel": "取消",
-		"delete": "删除",
-		"check_all": "全部检查",
-		"deletion_modal_header": "删除页",
-		"delete_completely": "完全删除",
+    "result_meta": "搜索结果:",
+    "deletion_mode_btn_lavel": "选择并删除页面",
+    "cancel": "取消",
+    "delete": "删除",
+    "check_all": "全部检查",
+    "deletion_modal_header": "删除页",
+    "delete_completely": "完全删除",
     "include_certain_path": "包含 {{pathToInclude}} 路径 ",
     "delete_all_selected_page": "删除所有",
     "currently_not_implemented": "这是当前未实现的功能",
-    "search_again" : "再次搜索",
-    "number_of_list_to_display" : "显示器的数量",
-    "page_number_unit" : "例",
-    "hit_number_unit" : "例",
+    "search_again": "再次搜索",
+    "number_of_list_to_display": "显示器的数量",
+    "page_number_unit": "例",
+    "hit_number_unit": "例",
     "sort_axis": {
       "relationScore": "按相关性排序",
       "createdAt": "按创建日期排序",
       "updatedAt": "按更新日期排序"
     }
-	},
+  },
   "private_legacy_pages": {
     "title": "私人遗留页面",
     "bulk_operation": "批量操作",
     "convert_all_selected_pages": "全部转换为新的v5兼容格式",
-		"input_path_to_convert": "输入一个转换页面的路径",
+    "input_path_to_convert": "输入一个转换页面的路径",
     "alert_title": "存在旧的v4兼容格式的私人网页。",
     "alert_desc1": "在这一页,你可以用复选框选择页面,并通过屏幕上方的批量操作按钮批量转换为新的v5兼容格式。",
     "nopages_title": "恭喜你。准备使用GROWI v5!",
@@ -588,14 +587,14 @@
       "error_duplicate_pages_found": "发现多个具有相同路径名称的页面。请重新命名或删除并重试。"
     }
   },
-	"login": {
+  "login": {
     "title": "登录",
-		"sign_in_error": "登录错误",
-		"registration_successful": "注册成功。请等待管理员批准",
-		"Setup": "安装程序",
-    "enabled_ldap_has_configuration_problem":"启用了LDAP,但配置有问题。",
+    "sign_in_error": "登录错误",
+    "registration_successful": "注册成功。请等待管理员批准",
+    "Setup": "安装程序",
+    "enabled_ldap_has_configuration_problem": "启用了LDAP,但配置有问题。",
     "set_env_var_for_logs": "(请设置环境变量 <code>DEBUG=crowi:service:PassportService</code> 以获得日志。)"
-	},
+  },
   "invited": {
     "invited": "邀请函",
     "discription_heading": "创建账户",
@@ -607,35 +606,35 @@
     "export_page_markdown": "以Markdown格式导出页面",
     "export_page_pdf": "以PDF格式导出页面"
   },
-	"message": {
-		"successfully_connected": "连接成功!",
-		"fail_to_save_access_token": "无法保存访问令牌。请再试一次。",
-		"fail_to_fetch_access_token": "无法获取访问令牌。请重新连接。",
-		"successfully_disconnected": "成功断开连接!",
+  "message": {
+    "successfully_connected": "连接成功!",
+    "fail_to_save_access_token": "无法保存访问令牌。请再试一次。",
+    "fail_to_fetch_access_token": "无法获取访问令牌。请重新连接。",
+    "successfully_disconnected": "成功断开连接!",
     "strategy_has_not_been_set_up": "{{strategy}} 尚未设置",
     "ldap_user_not_valid": "Ldap user is no valid",
     "external_account_not_exist": "查找或创建外部账户失败",
-		"maximum_number_of_users": "注册的用户数不能超过最大值。",
-		"sign_in_failure": "登录失败。",
-		"aws_sttings_required": "使用此功能所需的AWS设置。请询问管理员。",
-		"application_already_installed": "应用程序已安装。",
-		"email_address_could_not_be_used": "无法使用此电子邮件地址。(确保允许的电子邮件地址)",
+    "maximum_number_of_users": "注册的用户数不能超过最大值。",
+    "sign_in_failure": "登录失败。",
+    "aws_sttings_required": "使用此功能所需的AWS设置。请询问管理员。",
+    "application_already_installed": "应用程序已安装。",
+    "email_address_could_not_be_used": "无法使用此电子邮件地址。(确保允许的电子邮件地址)",
     "user_id_is_not_available": "此用户ID不可用。",
-    "username_should_not_be_null":"用户名不应为空。请检查管理页面上的身份验证机制设置",
-		"email_address_is_already_registered": "此电子邮件地址已注册。",
-		"can_not_register_maximum_number_of_users": "注册的用户数不能超过最大值。",
-    "email_settings_is_not_setup":"邮箱设置未设置,请询问管理员。",
+    "username_should_not_be_null": "用户名不应为空。请检查管理页面上的身份验证机制设置",
+    "email_address_is_already_registered": "此电子邮件地址已注册。",
+    "can_not_register_maximum_number_of_users": "注册的用户数不能超过最大值。",
+    "email_settings_is_not_setup": "邮箱设置未设置,请询问管理员。",
     "email_authentication_is_not_enabled": "电子邮件验证未被激活, 请询问管理员。",
-		"failed_to_register": "注册失败。",
-		"successfully_created": "已成功创建用户{{username}。",
-		"can_not_activate_maximum_number_of_users": "无法激活超过最大用户数的用户。",
-		"failed_to_activate": "无法激活。",
-		"unable_to_use_this_user": "无法使用此用户。",
-		"complete_to_install1": "完成安装GROWI!请以管理员帐户登录。",
-		"complete_to_install2": "完成安装GROWI!请先检查此页上的每个设置。",
-		"failed_to_create_admin_user": "无法创建管理用户。{{errMessage}",
-    "successfully_send_email_auth":"我们向 {{email}} 发送了一封电子邮件。 请点击邮件中的网址并完成注册。",
-    "incorrect_token_or_expired_url":"令牌不正确或 URL 已过期。",
+    "failed_to_register": "注册失败。",
+    "successfully_created": "已成功创建用户{{username}。",
+    "can_not_activate_maximum_number_of_users": "无法激活超过最大用户数的用户。",
+    "failed_to_activate": "无法激活。",
+    "unable_to_use_this_user": "无法使用此用户。",
+    "complete_to_install1": "完成安装GROWI!请以管理员帐户登录。",
+    "complete_to_install2": "完成安装GROWI!请先检查此页上的每个设置。",
+    "failed_to_create_admin_user": "无法创建管理用户。{{errMessage}",
+    "successfully_send_email_auth": "我们向 {{email}} 发送了一封电子邮件。 请点击邮件中的网址并完成注册。",
+    "incorrect_token_or_expired_url": "令牌不正确或 URL 已过期。",
     "user_already_logged_in": "当你登录的时候,你不能创建一个新的账户。",
     "registration_closed": "你无权创建一个新的账户。",
     "Username has invalid characters": "用户名有无效字符",
@@ -650,21 +649,21 @@
     "Password minimum character should be more than 6 characters": "密码最小字符应超过6个字符",
     "user_not_found": "未找到用户",
     "provider_duplicated_username_exception": "<p><strong><span class='material-symbols-outlined me-1'>cancel</span>发生了重复用户名异常</strong></p><p class='mb-0'> 你的 {{ failedProviderForDuplicatedUsernameException }} 认证成功了,但不能创建新的用户。参见问题<a href='https://github.com/weseek/growi/issues/193'>#193</a>.</p>"
-	},
-  "grid_edit":{
-    "create_bootstrap_4_grid":"创建Bootstrap 4网格",
+  },
+  "grid_edit": {
+    "create_bootstrap_4_grid": "创建Bootstrap 4网格",
     "grid_settings": "网格设置",
     "grid_pattern": "网格样式",
-    "division":"分割",
-    "smart_no":"手机/不分割",
-    "break_point":"按画面大小分割"
+    "division": "分割",
+    "smart_no": "手机/不分割",
+    "break_point": "按画面大小分割"
   },
-  "validation":{
+  "validation": {
     "aws_region": "关于地区,请输入AWS地区名,例如:ap-east-1",
     "aws_custom_endpoint": "关于自定义端点,请指定以http(s)://开头的URL,链接末尾不需要添加“/”",
-    "failed_to_send_a_test_email":"SMTP方式测试邮件发送失败,请检查相关设定。"
+    "failed_to_send_a_test_email": "SMTP方式测试邮件发送失败,请检查相关设定。"
   },
-  "forgot_password":{
+  "forgot_password": {
     "forgot_password": "忘记密码?",
     "send": "发送",
     "return_to_login": "返回登录",
@@ -677,11 +676,11 @@
     "email_is_required": "电子邮件是必需的",
     "success_to_send_email": "我发了一封电子邮件",
     "feature_is_unavailable": "此功能不可用",
-    "incorrect_token_or_expired_url":"令牌不正确或 URL 已过期。 请通过以下链接重新发送密码重置请求",
+    "incorrect_token_or_expired_url": "令牌不正确或 URL 已过期。 请通过以下链接重新发送密码重置请求",
     "password_and_confirm_password_does_not_match": "密码和确认密码不匹配",
     "please_enable_mailer_alert": "密码重置功能被禁用,因为电子邮件设置尚未完成。请要求管理员完成电子邮件的设置。"
   },
-  "emoji" :{
+  "emoji": {
     "title": "选择一个表情符号",
     "search": "搜索",
     "clear": "重置",
@@ -711,7 +710,7 @@
       "6": "深色肤色"
     }
   },
-  "maintenance_mode":{
+  "maintenance_mode": {
     "maintenance_mode": "维护模式",
     "growi_is_under_maintenance": "GROWI正在进行维护。请等待,直到它结束。",
     "admin_page": "管理员页",
@@ -723,10 +722,10 @@
     "you_cannot_move_this_page_now": "你现在不能移动这个页面",
     "something_went_wrong_with_moving_page": "移动页面时出了问题"
   },
-  "duplicated_page_alert" : {
+  "duplicated_page_alert": {
     "same_page_name_exists": "页面名称「{{pageName}}」是重复的",
-    "same_page_name_exists_at_path" : "在”{{path}}” 中,有不止一个名为”{{pageName}}”的页面",
-    "select_page_to_see" : "请在下面选择你想去的页面。"
+    "same_page_name_exists_at_path": "在”{{path}}” 中,有不止一个名为”{{pageName}}”的页面",
+    "select_page_to_see": "请在下面选择你想去的页面。"
   },
   "user_group": {
     "select_group": "选择组别",
@@ -773,9 +772,9 @@
       }
     }
   },
-  "page_operation":{
+  "page_operation": {
     "paths_recovered": "成功恢复了页面路径",
-    "path_recovery_failed":"路径恢复失败"
+    "path_recovery_failed": "路径恢复失败"
   },
   "footer": {
     "bookmarks": "书签",
@@ -845,6 +844,8 @@
     "fail_publish_page": "无法停用 WIP"
   },
   "sidebar_header": {
-    "show_wip_page": "显示 WIP"
+    "show_wip_page": "显示 WIP",
+    "size_s": "尺寸: S",
+    "size_l": "尺寸: L"
   }
 }

+ 7 - 0
apps/app/src/components/LoadingSpinner.jsx

@@ -0,0 +1,7 @@
+import React from 'react';
+
+import styles from './LoadingSpinner.module.scss';
+
+export const LoadingSpinner = () => (
+  <span className={`material-symbols-outlined pb-0 ${styles.spinner}`}>progress_activity</span>
+);

+ 39 - 0
apps/app/src/components/LoadingSpinner.module.scss

@@ -0,0 +1,39 @@
+.spinner {
+  animation: animation-rotate 750ms infinite linear;
+}
+
+// refs: https://github.com/weseek/growi/blob/master/apps/app/src/styles/atoms/_spinners.scss
+@keyframes animation-rotate {
+  100% {
+    transform: rotate(360deg);
+    transform: rotate(360deg);
+  }
+}
+
+@-o-keyframes animation-rotate {
+  100% {
+    -o-transform: rotate(360deg);
+    transform: rotate(360deg);
+  }
+}
+
+@-ms-keyframes animation-rotate {
+  100% {
+    -ms-transform: rotate(360deg);
+    transform: rotate(360deg);
+  }
+}
+
+@-webkit-keyframes animation-rotate {
+  100% {
+    -webkit-transform: rotate(360deg);
+    transform: rotate(360deg);
+  }
+}
+
+@-moz-keyframes animation-rotate {
+  100% {
+    -moz-transform: rotate(360deg);
+    transform: rotate(360deg);
+  }
+}

+ 11 - 5
apps/app/src/components/LoginForm.tsx

@@ -14,7 +14,7 @@ import { RegistrationMode } from '~/interfaces/registration-mode';
 import { toArrayIfNot } from '~/utils/array-utils';
 
 import { CompleteUserRegistration } from './CompleteUserRegistration';
-
+import { LoadingSpinner } from './LoadingSpinner';
 
 import styles from './LoginForm.module.scss';
 
@@ -238,8 +238,11 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
             >
               <div className="eff"></div>
               <span className="btn-label">
-                {/* spinner.Tentative decision meiri-k 11.17 */}
-                <span className="material-symbols-outlined">{isLoading ? 'hoge' : 'login'}</span>
+                {isLoading ? (
+                  <LoadingSpinner />
+                ) : (
+                  <span className="material-symbols-outlined">login</span>
+                )}
               </span>
               <span className="btn-label-text">{t('Sign in')}</span>
             </button>
@@ -513,8 +516,11 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
             >
               <div className="eff"></div>
               <span className="btn-label">
-                {/* spinner.Tentative decision meiri-k 11.17 */}
-                <span className="material-symbols-outlined">{isLoading ? 'hoge' : 'login'}</span>
+                {isLoading ? (
+                  <LoadingSpinner />
+                ) : (
+                  <span className="material-symbols-outlined">login</span>
+                )}
               </span>
               <span className="btn-label-text">{submitText}</span>
             </button>

+ 6 - 0
apps/app/src/components/PageEditor/ConflictDiffModal.module.scss

@@ -0,0 +1,6 @@
+// TODO: https://redmine.weseek.co.jp/issues/142208
+.conflict-diff-modal :global {
+  .cm-editor {
+    height: 400px !important;
+  }
+}

+ 323 - 0
apps/app/src/components/PageEditor/ConflictDiffModal.tsx

@@ -0,0 +1,323 @@
+
+import React, {
+  useState, useEffect, useCallback, useMemo,
+} from 'react';
+
+import type { IRevisionOnConflict } from '@growi/core';
+import {
+  MergeViewer, CodeMirrorEditorDiff, GlobalCodeMirrorEditorKey, useCodeMirrorEditorIsolated,
+} from '@growi/editor';
+import { UserPicture } from '@growi/ui/dist/components';
+import { format } from 'date-fns';
+import { useTranslation } from 'next-i18next';
+import {
+  Modal, ModalHeader, ModalBody, ModalFooter,
+} from 'reactstrap';
+
+import { toastError, toastSuccess } from '~/client/util/toastr';
+import { useCurrentPathname, useCurrentUser } from '~/stores/context';
+import { useConflictDiffModal } from '~/stores/modal';
+import { useCurrentPagePath, useSWRxCurrentPage, useCurrentPageId } from '~/stores/page';
+import {
+  useRemoteRevisionBody, useRemoteRevisionId, useRemoteRevisionLastUpdatedAt, useRemoteRevisionLastUpdateUser, useSetRemoteLatestPageData,
+} from '~/stores/remote-latest-page';
+
+import styles from './ConflictDiffModal.module.scss';
+
+type ConflictDiffModalCoreProps = {
+  // optionsToSave: OptionsToSave | undefined;
+  request: IRevisionOnConflictWithStringDate,
+  latest: IRevisionOnConflictWithStringDate,
+  onClose?: () => void,
+  onResolved?: () => void,
+};
+
+type IRevisionOnConflictWithStringDate = Omit<IRevisionOnConflict, 'createdAt'> & {
+  createdAt: string
+}
+
+const ConflictDiffModalCore = (props: ConflictDiffModalCoreProps): JSX.Element => {
+  const {
+    request, latest, onClose, onResolved,
+  } = props;
+
+  const [resolvedRevision, setResolvedRevision] = useState<string>('');
+  const [isRevisionselected, setIsRevisionSelected] = useState<boolean>(false);
+  const [revisionSelectedToggler, setRevisionSelectedToggler] = useState<boolean>(false);
+  const [isModalExpanded, setIsModalExpanded] = useState<boolean>(false);
+
+  const { t } = useTranslation();
+  const { data: conflictDiffModalStatus, close: closeConflictDiffModal } = useConflictDiffModal();
+  const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.DIFF);
+
+  // const { data: remoteRevisionId } = useRemoteRevisionId();
+  // const { setRemoteLatestPageData } = useSetRemoteLatestPageData();
+  // const { data: pageId } = useCurrentPageId();
+  // const { data: currentPagePath } = useCurrentPagePath();
+  // const { data: currentPathname } = useCurrentPathname();
+
+  const selectRevisionHandler = useCallback((selectedRevision: string) => {
+    setResolvedRevision(selectedRevision);
+    setRevisionSelectedToggler(prev => !prev);
+
+    if (!isRevisionselected) {
+      setIsRevisionSelected(true);
+    }
+  }, [isRevisionselected]);
+
+  const closeModalHandler = useCallback(() => {
+    closeConflictDiffModal();
+    onClose?.();
+  }, [closeConflictDiffModal, onClose]);
+
+  const resolveConflictHandler = useCallback(async() => {
+    const newBody = codeMirrorEditor?.getDoc();
+
+    // TODO: impl
+    onResolved?.();
+  }, [codeMirrorEditor, onResolved]);
+
+  useEffect(() => {
+    codeMirrorEditor?.initDoc(resolvedRevision);
+    // Enable selecting the same revision after editing by including revisionSelectedToggler in the dependency array of useEffect
+  }, [codeMirrorEditor, resolvedRevision, revisionSelectedToggler]);
+
+  const headerButtons = useMemo(() => (
+    <div className="d-flex align-items-center">
+      <button type="button" className="btn" onClick={() => setIsModalExpanded(prev => !prev)}>
+        <span className="material-symbols-outlined">{isModalExpanded ? 'close_fullscreen' : 'open_in_full'}</span>
+      </button>
+      <button type="button" className="btn" onClick={closeModalHandler} aria-label="Close">
+        <span className="material-symbols-outlined">close</span>
+      </button>
+    </div>
+  ), [closeModalHandler, isModalExpanded]);
+
+  return (
+    <Modal isOpen={conflictDiffModalStatus?.isOpened} className={`${styles['conflict-diff-modal']} ${isModalExpanded ? ' grw-modal-expanded' : ''}`} size="xl">
+
+      <ModalHeader tag="h4" className="d-flex align-items-center" close={headerButtons}>
+        <span className="material-symbols-outlined me-1">error</span>{t('modal_resolve_conflict.resolve_conflict')}
+      </ModalHeader>
+
+      <ModalBody className="mx-4 my-1">
+        <div className="row">
+          <div className="col-12 text-center mt-2 mb-4">
+            <h3 className="fw-bold text-muted">{t('modal_resolve_conflict.resolve_conflict_message')}</h3>
+          </div>
+
+          <div className="col-6">
+            <h4 className="fw-bold my-2 text-muted">{t('modal_resolve_conflict.requested_revision')}</h4>
+            <div className="d-flex align-items-center my-3">
+              <div>
+                <UserPicture user={request.user} size="lg" noLink noTooltip />
+              </div>
+              <div className="ms-3 text-muted">
+                <p className="my-0">updated by {request.user.username}</p>
+                <p className="my-0">{request.createdAt}</p>
+              </div>
+            </div>
+          </div>
+
+          <div className="col-6">
+            <h4 className="fw-bold my-2 text-muted">{t('modal_resolve_conflict.latest_revision')}</h4>
+            <div className="d-flex align-items-center my-3">
+              <div>
+                <UserPicture user={latest.user} size="lg" noLink noTooltip />
+              </div>
+              <div className="ms-3 text-muted">
+                <p className="my-0">updated by {latest.user.username}</p>
+                <p className="my-0">{latest.createdAt}</p>
+              </div>
+            </div>
+          </div>
+
+          <MergeViewer
+            leftBody={request.revisionBody}
+            rightBody={latest.revisionBody}
+          />
+
+          <div className="col-6">
+            <div className="text-center my-4">
+              <button
+                type="button"
+                className="btn btn-outline-primary"
+                onClick={() => { selectRevisionHandler(request.revisionBody) }}
+              >
+                <span className="material-symbols-outlined me-1">arrow_circle_down</span>
+                {t('modal_resolve_conflict.select_revision', { revision: 'mine' })}
+              </button>
+            </div>
+          </div>
+
+          <div className="col-6">
+            <div className="text-center my-4">
+              <button
+                type="button"
+                className="btn btn-outline-primary"
+                onClick={() => { selectRevisionHandler(latest.revisionBody) }}
+              >
+                <span className="material-symbols-outlined me-1">arrow_circle_down</span>
+                {t('modal_resolve_conflict.select_revision', { revision: 'theirs' })}
+              </button>
+            </div>
+          </div>
+
+          <div className="col-12">
+            <div className="border border-dark">
+              <h4 className="fw-bold my-2 mx-2 text-muted">{t('modal_resolve_conflict.selected_editable_revision')}</h4>
+              <CodeMirrorEditorDiff />
+            </div>
+          </div>
+        </div>
+      </ModalBody>
+
+      <ModalFooter>
+        <button
+          type="button"
+          className="btn btn-outline-secondary"
+          onClick={closeModalHandler}
+        >
+          {t('Cancel')}
+        </button>
+        <button
+          type="button"
+          className="btn btn-primary ms-3"
+          onClick={resolveConflictHandler}
+          disabled={!isRevisionselected}
+        >
+          {t('modal_resolve_conflict.resolve_and_save')}
+        </button>
+      </ModalFooter>
+    </Modal>
+  );
+};
+
+
+const dummyTest1 = `# :tada: グローウィ へようこそ
+[![GitHub Releases](https://img.shields.io/github/release/weseek/growi.svg)](https://github.com/weseek/growi/releases/latest)
+[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://github.com/weseek/growi/blob/master/LICENSE)
+
+グローウィ は個人・法人向けの Wiki | ナレッジベースツールです。
+会社や大学の研究室、サークルでのナレッジ情報を簡単に共有でき、作られたページは誰でも編集が可能です。
+
+知っている情報をカジュアルに書き出しみんなで編集することで、**チーム内での暗黙知を減らす**ことができます。
+当たり前に共有される情報を日々増やしていきましょう。
+
+### :beginner: 簡単なページの作り方
+
+- 右上の "**作成**"ボタンまたは右下の**鉛筆アイコン**のボタンからページを書き始めることができます
+    - ページタイトルは後から変更できますので、適当に入力しても大丈夫です
+        - タイトル入力欄では、半角の / (スラッシュ) でページ階層を作れます
+        - (例)/カテゴリ1/カテゴリ2/作りたいページタイトル のように入力してみてください
+- \`\`- \` を行頭につけると、この文章のような箇条書きを書くことができます\`\`
+- 画像やPDF、Word/Excel/PowerPointなどの添付ファイルも、コピー&ペースト、ドラッグ&ドロップで貼ることができます
+- 書けたら "**更新**" ボタンを押してページを公開しましょう
+    - \`Ctrl(⌘) + S\` でも保存できます
+
+さらに詳しくはこちら: [ページを作成する](https://docs.growi.org/ja/guide/features/create_page.html)
+
+<div class="mt-4 card border-primary">
+  <div class="card-header bg-primary text-light">Tips</div>
+  <div class="card-body"><ul>
+    <li>Ctrl(⌘) + "/" でショートカットヘルプを表示します</li>
+    <li>HTML/CSS の記述には、<a href="https://getbootstrap.com/docs/4.6/components/">Bootstrap 4</a> を利用できます</li>
+  </ul></div>
+</div>
+`;
+
+const dummyTest2 = `# :tada: GROWI へようこそ
+[![GitHub Releases](https://img.shields.io/github/release/weseek/growi.svg)](https://github.com/weseek/growi/releases/latest)
+[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://github.com/weseek/growi/blob/master/LICENSE)
+
+GROWI は個人・法人向けの Wiki | ナレッジベースツールです。
+会社や大学の研究室、サークルでのナレッジ情報を簡単に共有でき、作られたページは誰でも編集が可能です。
+
+知っている情報をカジュアルに書き出しみんなで編集することで、**チーム内での暗黙知を減らす**ことができます。
+当たり前に共有される情報を日々増やしていきましょう。
+
+### :beginner: 簡単なページの作り方
+
+- 右上の "**作成**"ボタンまたは右下の**鉛筆アイコン**のボタンからページを書き始めることができます
+    - ページタイトルは後から変更できますので、適当に入力しても大丈夫です
+        - タイトル入力欄では、半角の / (スラッシュ) でページ階層を作れます
+        - (例)/カテゴリ1/カテゴリ2/作りたいページタイトル のように入力してみてください
+- \`\`- \` を行頭につけると、この文章のような箇条書きを書くことができます\`\`
+- 画像やPDF、Word/Excel/PowerPointなどの添付ファイルも、コピー&ペースト、ドラッグ&ドロップで貼ることができます
+- 書けたら "**更新**" ボタンを押してページを公開しましょう
+    - \`Ctrl(⌘) + S\` でも保存できます
+
+さらに詳しくはこちら: [ページを作成する](https://docs.growi.org/ja/guide/features/create_page.html)
+
+<div class="mt-4 card border-primary">
+  <div class="card-header bg-primary text-light">Tips</div>
+  <div class="card-body"><ul>
+    <li>Ctrl(⌘) + "/" でショートカットヘルプを表示します</li>
+    <li>HTML/CSS の記述には、<a href="https://getbootstrap.com/docs/4.6/components/">Bootstrap 4</a> を利用できます</li>
+  </ul></div>
+</div>
+`;
+
+type ConflictDiffModalProps = {
+  onClose?: () => void,
+  onResolved?: () => void,
+  // optionsToSave: OptionsToSave | undefined;
+  // afterResolvedHandler: () => void,
+};
+
+
+export const ConflictDiffModal = (props: ConflictDiffModalProps): JSX.Element => {
+  const {
+    onClose, onResolved,
+  } = props;
+  const { data: currentUser } = useCurrentUser();
+
+  // 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 { data: conflictDiffModalStatus } = useConflictDiffModal();
+
+  const currentTime: Date = new Date();
+
+  const isRemotePageDataInappropriate = remoteRevisionId == null || remoteRevisionBody == null || remoteRevisionLastUpdateUser == null;
+
+  if (!conflictDiffModalStatus?.isOpened || currentUser == null || currentPage == null) {
+    return <></>;
+  }
+
+  const request: IRevisionOnConflictWithStringDate = {
+    revisionId: '',
+    revisionBody: dummyTest1,
+    createdAt: format(currentTime, 'yyyy/MM/dd HH:mm:ss'),
+    user: currentUser,
+  };
+
+  const latest: IRevisionOnConflictWithStringDate = {
+    revisionId: '',
+    revisionBody: dummyTest2,
+    createdAt: format(currentTime, 'yyyy/MM/dd HH:mm:ss'),
+    user: currentUser,
+  };
+
+  // const latest: IRevisionOnConflictWithStringDate = {
+  //   revisionId: remoteRevisionId,
+  //   revisionBody: remoteRevisionBody,
+  //   createdAt: format(new Date(remoteRevisionLastUpdatedAt || currentTime.toString()), 'yyyy/MM/dd HH:mm:ss'),
+  //   user: remoteRevisionLastUpdateUser,
+  // };
+
+  const propsForCore = {
+    onResolved,
+    onClose,
+    request,
+    latest,
+  };
+
+  return <ConflictDiffModalCore {...propsForCore} />;
+};

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

@@ -9,8 +9,8 @@ const OptionsSelector = dynamic(() => import('~/components/PageEditor/OptionsSel
 
 const EditorNavbarBottom = (): JSX.Element => {
   return (
-    <div data-testid="grw-editor-navbar-bottom">
-      <div className={`flex-expand-horiz align-items-center px-2 px-md-3 ${moduleClass}`}>
+    <div className="border-top" data-testid="grw-editor-navbar-bottom">
+      <div className={`flex-expand-horiz align-items-center px-2 py-1 py-md-2 px-md-3 ${moduleClass}`}>
         <form className="m-2 me-auto">
           <OptionsSelector />
         </form>

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

@@ -278,7 +278,7 @@ export const OptionsSelector = (): JSX.Element => {
   return (
     <Dropdown isOpen={dropdownOpen} toggle={() => { setStatus(OptionsStatus.Home); setDropdownOpen(!dropdownOpen) }} direction="up" className="">
       <DropdownToggle
-        className={`btn btn-outline-neutral-secondary d-flex align-items-center justify-content-center
+        className={`btn btn-sm btn-outline-neutral-secondary d-flex align-items-center justify-content-center
               ${isDeviceLargerThanMd ? 'border border-secondary' : 'border-0'}
               ${dropdownOpen ? 'active' : ''}
               `}

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

@@ -436,7 +436,7 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
       <EditorNavbar />
 
       <div className={`flex-expand-horiz ${props.visibility ? '' : 'd-none'}`}>
-        <div className="page-editor-editor-container flex-expand-vert">
+        <div className="page-editor-editor-container flex-expand-vert border-end">
           <CodeMirrorEditorMain
             onChange={markdownChangedHandler}
             onSave={saveWithShortcut}
@@ -454,7 +454,7 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
         <div
           ref={previewRef}
           onScroll={scrollPreviewHandlerThrottle}
-          className="page-editor-preview-container flex-expand-vert d-none d-lg-flex"
+          className="page-editor-preview-container flex-expand-vert overflow-y-auto d-none d-lg-flex"
         >
           <Preview
             rendererOptions={rendererOptions}

+ 3 - 14
apps/app/src/components/Sidebar/RecentChanges/RecentChangesSubstance.module.scss

@@ -3,23 +3,13 @@
 .grw-recent-changes-resize-button :global {
   line-height: normal;
   transform: translateY(-2px);
-
-  .form-check-label::before {
-    padding-left: 5px;
-    content: 'L';
-  }
-
-  .form-check-input:checked + .form-check-label::before {
-    padding-left: 5px;
-    content: 'S';
-  }
 }
 
 .list-group-item :global {
   font-size: 12px;
 
   .grw-recent-changes-skeleton-small {
-    @include grw-skeleton-text($font-size:14px, $line-height:16px);
+    @include grw-skeleton-text($font-size: 14px, $line-height: 16px);
     max-width: 120px;
   }
 
@@ -29,7 +19,7 @@
   }
 
   .grw-recent-changes-skeleton-date {
-    @include grw-skeleton-text($font-size:10px, $line-height:12px);
+    @include grw-skeleton-text($font-size: 10px, $line-height: 12px);
     width: 80px;
   }
 
@@ -43,7 +33,6 @@
   }
 }
 
-
 .grw-recent-changes-item-lower :global {
   font-size: 12px;
 
@@ -57,7 +46,7 @@
 
 // == Colors
 .grw-former-link a {
-  --bs-link-opacity: .5;
+  --bs-link-opacity: 0.5;
 
   &:global {
     &:hover {

+ 6 - 4
apps/app/src/components/Sidebar/RecentChanges/RecentChangesSubstance.tsx

@@ -189,7 +189,7 @@ export const RecentChangesHeader = ({
 
         <DropdownMenu container="body">
           <DropdownItem onClick={changeSizeHandler}>
-            <div className={`${styles['grw-recent-changes-resize-button']} form-check form-switch`}>
+            <div className={`${styles['grw-recent-changes-resize-button']} form-check form-switch mb-0`}>
               <input
                 id="recentChangesResize"
                 className="form-check-input"
@@ -197,12 +197,14 @@ export const RecentChangesHeader = ({
                 checked={isSmall}
                 onChange={() => {}}
               />
-              <label className="form-label form-check-label text-muted" htmlFor="recentChangesResize" />
+              <label className="form-label form-check-label text-muted mb-0" htmlFor="recentChangesResize">
+                {isSmall ? t('sidebar_header.size_s') : t('sidebar_header.size_l')}
+              </label>
             </div>
           </DropdownItem>
 
           <DropdownItem onClick={onWipPageShownChange}>
-            <div className="form-check form-switch">
+            <div className="form-check form-switch mb-0">
               <input
                 id="wipPageVisibility"
                 className="form-check-input"
@@ -210,7 +212,7 @@ export const RecentChangesHeader = ({
                 checked={isWipPageShown}
                 onChange={() => {}}
               />
-              <label className="form-label form-check-label text-muted" htmlFor="wipPageVisibility">
+              <label className="form-label form-check-label text-muted mb-0" htmlFor="wipPageVisibility">
                 {t('sidebar_header.show_wip_page')}
               </label>
             </div>

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

@@ -149,7 +149,7 @@ export const getPageSchema = (crowi) => {
       return true;
     }
 
-    const revision = this.latestRevision || this.revision;
+    const revision = this.latestRevision || this.revision._id;
     // comparing ObjectId with string
     // eslint-disable-next-line eqeqeq
     if (revision != previousRevision) {

+ 2 - 1
apps/app/src/server/routes/apiv3/page/update-page.ts

@@ -136,7 +136,8 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
 
       // check revision
       const currentPage = await Page.findByIdAndViewer(pageId, req.user);
-      if (currentPage != null && !currentPage.isUpdatable(revisionId, origin)) {
+
+      if (currentPage != null && !await currentPage.isUpdatable(revisionId, origin)) {
         const latestRevision = await Revision.findById(currentPage.revision).populate('author');
         const returnLatestRevision = {
           revisionId: latestRevision?._id.toString(),

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

@@ -27,7 +27,6 @@
    * Expand Editor
    *****************/
   .dynamic-layout-root {
-    width: calc(100vw - var.$grw-sidebar-nav-width);
     @extend .flex-expand-vh-100;
   }
 

+ 6 - 2
packages/core/scss/_flex-expand.scss

@@ -13,9 +13,13 @@
 .flex-expand-vh-100 {
   height: 100vh;
 
-  .flex-expand-horiz,
-  .flex-expand-vert {
+  .flex-expand-horiz {
     height: 100%;
     overflow-y: auto;
   }
+
+  .flex-expand-vert {
+    height: 100%;
+    overflow-y: hidden;
+  }
 }

+ 0 - 1
packages/custom-icons/svg/drawer_io.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24"><defs><style>.a,.c{fill:none;}.a{stroke:#707070;}.b{clip-path:url(#a);}</style><clipPath id="a"><rect class="a" width="24" height="24" transform="translate(-14953 -18037)"/></clipPath></defs><g class="b" transform="translate(14953 18037)"><path class="c" d="M70.158,9.041H67.433L65.921,6.46a.909.909,0,0,0,.35-.706V1.477a.942.942,0,0,0-.95-.932H60.932a.942.942,0,0,0-.95.932V5.755a.911.911,0,0,0,.35.706l-1.511,2.58H56.057a.942.942,0,0,0-.949.932v4.277a.942.942,0,0,0,.949.932h4.389a.941.941,0,0,0,.949-.932V9.972a.941.941,0,0,0-.949-.932h-.107l1.38-2.354h2.815l1.379,2.354H65.77a.942.942,0,0,0-.95.932v4.277a.942.942,0,0,0,.95.932h4.388a.942.942,0,0,0,.95-.932V9.972a.942.942,0,0,0-.95-.932m-10.08,4.848H56.425V10.333h3.653ZM61.3,1.838h3.653V5.394H61.3Zm8.491,12.051H66.137V10.333h3.653Z" transform="translate(-15004.108 -18032.863)"/></g></svg>

+ 0 - 1
packages/custom-icons/svg/header.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24"><defs><style>.a{fill:#fff;}.b{clip-path:url(#a);}.c{fill:none;}</style><clipPath id="a"><rect class="a" width="24" height="24" transform="translate(-14908 -18037)"/></clipPath></defs><g class="b" transform="translate(14908 18037)"><path class="c" d="M10.813.26v7H2.008v-7H0v16H2.008v-7h8.805v7h2.008V.26Z" transform="translate(-14902.41 -18033.26)"/></g></svg>

+ 3 - 1
packages/editor/package.json

@@ -22,12 +22,14 @@
     "react-dom": "^18.2.0"
   },
   "// comments for devDependencies": {
-    "string-width": "5.0.0 or above exports only ESM."
+    "string-width": "5.0.0 or above exports only ESM.",
+    "@codemirror/merge": "Fixed version at 6.0.0 due to errors caused by dependent packages"
   },
   "devDependencies": {
     "@codemirror/lang-markdown": "^6.2.0",
     "@codemirror/language": "^6.8.0",
     "@codemirror/language-data": "^6.3.1",
+    "@codemirror/merge": "6.0.0",
     "@codemirror/state": "^6.2.1",
     "@codemirror/view": "^6.15.3",
     "@popperjs/core": "^2.11.8",

+ 1 - 1
packages/editor/src/components/CodeMirrorEditor/CodeMirrorEditor.tsx

@@ -203,7 +203,7 @@ export const CodeMirrorEditor = (props: Props): JSX.Element => {
   }, [isUploading, isDragAccept, isDragReject, acceptedUploadFileType]);
 
   return (
-    <div className={`${style['codemirror-editor']} flex-expand-vert`}>
+    <div className={`${style['codemirror-editor']} flex-expand-vert overflow-y-hidden`}>
       <div {...getRootProps()} className={`dropzone ${fileUploadState} flex-expand-vert`}>
         <input {...getInputProps()} />
         <FileDropzoneOverlay isEnabled={isDragActive} />

+ 21 - 0
packages/editor/src/components/CodeMirrorEditor/Toolbar/AttachmentsDropup.module.scss

@@ -0,0 +1,21 @@
+.btn-attachment-toggle {
+  $color-active: var(--bs-tertiary-color);
+  $color-active-rgb: var(--bs-tertiary-color-rgb);
+
+  --bs-btn-color: var(--bs-tertiary-color);
+  --bs-btn-bg: var(--bs-secondary-bg);
+
+  --bs-btn-hover-color: #{$color-active};
+  --bs-btn-hover-bg: rgba(#{$color-active-rgb}, 0.2);
+
+  --bs-btn-active-color: #{$color-active};
+  // --bs-btn-active-bg: transparent;
+
+  --bs-btn-border-width: 0;
+
+  &:global {
+    &:hover {
+      --bs-btn-active-bg: rgba(#{$color-active-rgb}, 0.2);
+    }
+  }
+}

+ 6 - 1
packages/editor/src/components/CodeMirrorEditor/Toolbar/AttachmentsDropup.tsx

@@ -13,6 +13,11 @@ import type { GlobalCodeMirrorEditorKey } from '../../../consts';
 import { AttachmentsDropdownItem } from './AttachmentsDropdownItem';
 import { LinkEditButton } from './LinkEditButton';
 
+import styles from './AttachmentsDropup.module.scss';
+
+const btnAttachmentToggleClass = styles['btn-attachment-toggle'];
+
+
 type Props = {
   editorKey: string | GlobalCodeMirrorEditorKey,
   acceptedUploadFileType: AcceptedUploadFileType,
@@ -27,7 +32,7 @@ export const AttachmentsDropup = (props: Props): JSX.Element => {
   return (
     <>
       <Dropdown isOpen={isOpen} toggle={() => setOpen(!isOpen)} direction="up" className="lh-1">
-        <DropdownToggle className="btn-toolbar-button rounded-circle">
+        <DropdownToggle className={`${btnAttachmentToggleClass} btn-toolbar-button rounded-circle`} color="unset">
           <span className="material-symbols-outlined fs-6">add</span>
         </DropdownToggle>
         <DropdownMenu>

+ 3 - 1
packages/editor/src/components/CodeMirrorEditor/Toolbar/DiagramButton.tsx

@@ -14,7 +14,9 @@ export const DiagramButton = (props: Props): JSX.Element => {
   }, [editorKey, openDrawioModal]);
   return (
     <button type="button" className="btn btn-toolbar-button" onClick={onClickDiagramButton}>
-      <span className="growi-custom-icons">drawer_io</span>
+      {/* TODO: https://github.com/weseek/growi/pull/8558 */}
+      {/* <span className="growi-custom-icons">drawer_io</span> */}
+      <span className="material-symbols-outlined fs-5">block</span>
     </button>
   );
 };

+ 3 - 1
packages/editor/src/components/CodeMirrorEditor/Toolbar/TextFormatTools.tsx

@@ -69,7 +69,9 @@ export const TextFormatTools = (props: TextFormatToolsType): JSX.Element => {
             <span className="material-symbols-outlined fs-5">format_strikethrough</span>
           </button>
           <button type="button" className="btn btn-toolbar-button" onClick={() => onClickInsertPrefix('#', true)}>
-            <span className="growi-custom-icons">header</span>
+            {/* TODO: https://github.com/weseek/growi/pull/8558 */}
+            {/* <span className="growi-custom-icons">header</span> */}
+            <span className="material-symbols-outlined fs-5">block</span>
           </button>
           <button type="button" className="btn btn-toolbar-button" onClick={() => onClickInsertMarkdownElements('`', '`')}>
             <span className="material-symbols-outlined fs-5">code</span>

+ 1 - 1
packages/editor/src/components/CodeMirrorEditor/Toolbar/Toolbar.tsx

@@ -23,7 +23,7 @@ export const Toolbar = memo((props: Props): JSX.Element => {
 
   const { editorKey, acceptedUploadFileType, onUpload } = props;
   return (
-    <div className={`d-flex gap-2 p-2 codemirror-editor-toolbar ${styles['codemirror-editor-toolbar']}`}>
+    <div className={`d-flex gap-2 py-1 px-2 px-md-3 border-top codemirror-editor-toolbar ${styles['codemirror-editor-toolbar']}`}>
       <AttachmentsDropup editorKey={editorKey} onUpload={onUpload} acceptedUploadFileType={acceptedUploadFileType} />
       <TextFormatTools editorKey={editorKey} />
       <EmojiButton

+ 38 - 0
packages/editor/src/components/CodeMirrorEditorDiff.tsx

@@ -0,0 +1,38 @@
+import { useEffect, useRef, useMemo } from 'react';
+
+import type { Extension } from '@codemirror/state';
+import { placeholder, scrollPastEnd } from '@codemirror/view';
+import type { ReactCodeMirrorProps } from '@uiw/react-codemirror';
+
+import { GlobalCodeMirrorEditorKey } from '../consts';
+import { useCodeMirrorEditorIsolated, useDefaultExtensions, useEditorSettings } from '../stores';
+
+const additionalExtensions: Extension[] = [
+  [
+    // todo: i18n
+    placeholder('Please select page body'),
+    scrollPastEnd(),
+  ],
+];
+
+export const CodeMirrorEditorDiff = (): JSX.Element => {
+  const codeMirrorRef = useRef(null);
+
+  const cmProps = useMemo<ReactCodeMirrorProps>(() => {
+    return {};
+  }, []);
+
+  const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.DIFF, codeMirrorRef.current, cmProps);
+
+  useDefaultExtensions(codeMirrorEditor);
+  useEditorSettings(codeMirrorEditor);
+
+  // setup additional extensions
+  useEffect(() => {
+    return codeMirrorEditor?.appendExtensions?.(additionalExtensions);
+  }, [codeMirrorEditor]);
+
+  return (
+    <div ref={codeMirrorRef} />
+  );
+};

+ 40 - 0
packages/editor/src/components/MergeViewer.tsx

@@ -0,0 +1,40 @@
+import { memo, useEffect, useRef } from 'react';
+
+import { MergeView } from '@codemirror/merge';
+import { type Extension, EditorState } from '@codemirror/state';
+import { EditorView, basicSetup } from 'codemirror';
+
+type Props = {
+  leftBody: string
+  rightBody: string
+}
+
+const MergeViewerExtensions: Extension = [
+  basicSetup,
+  EditorView.editable.of(false),
+  EditorState.readOnly.of(true),
+];
+
+export const MergeViewer = memo(({ leftBody, rightBody }: Props) => {
+  const mergeViewRef = useRef<HTMLDivElement>(null);
+
+  useEffect(() => {
+    if (mergeViewRef.current != null) {
+      const view = new MergeView({
+        a: {
+          doc: leftBody,
+          extensions: MergeViewerExtensions,
+        },
+        b: {
+          doc: rightBody,
+          extensions: MergeViewerExtensions,
+        },
+        parent: mergeViewRef.current,
+      });
+
+      return () => view.destroy();
+    }
+  });
+
+  return <div ref={mergeViewRef} />;
+});

+ 2 - 0
packages/editor/src/components/index.ts

@@ -1,3 +1,5 @@
 export * from './CodeMirrorEditor';
 export * from './CodeMirrorEditorMain';
 export * from './CodeMirrorEditorComment';
+export * from './CodeMirrorEditorDiff';
+export * from './MergeViewer';

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

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

+ 8 - 0
yarn.lock

@@ -1506,6 +1506,14 @@
     "@codemirror/view" "^6.0.0"
     crelt "^1.0.5"
 
+"@codemirror/merge@6.0.0":
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/@codemirror/merge/-/merge-6.0.0.tgz#69c4877437bc0a75ff9984da1f5dabb5d88301a6"
+  integrity sha512-dxdUIQRxgC+xqzBtfY5zjgDIR38Xp6iycb8Lp1Q2gzEkX9y/UrqOAOlpqU3kfDBa0wGHrjlSYzpcQ/lXWG/59w==
+  dependencies:
+    "@codemirror/state" "^6.0.0"
+    "@codemirror/view" "^6.0.0"
+
 "@codemirror/search@^6.0.0":
   version "6.5.0"
   resolved "https://registry.yarnpkg.com/@codemirror/search/-/search-6.5.0.tgz#308f9968434e0e6ed59c9ec36a0239eb1dfc5d92"