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

Merge branch 'dev/7.0.x' into imprv/133910-141781-g2g-transfer-loading-icon

Tatsuya Ise 2 лет назад
Родитель
Сommit
8d723d8cda
100 измененных файлов с 1163 добавлено и 1064 удалено
  1. 1 1
      .github/workflows/ci-app-prod.yml
  2. 1 1
      .mergify.yml
  3. 29 1
      CHANGELOG.md
  4. 0 4
      apps/app/_obsolete/src/components/Navbar/GrowiNavbar.module.scss
  5. 2 2
      apps/app/_obsolete/src/components/PageEditor/Editor.tsx
  6. 2 4
      apps/app/_obsolete/src/components/Sidebar/AppearanceModeDropdown.tsx
  7. 75 68
      apps/app/public/static/locales/en_US/translation.json
  8. 73 66
      apps/app/public/static/locales/ja_JP/translation.json
  9. 376 369
      apps/app/public/static/locales/zh_CN/translation.json
  10. 3 1
      apps/app/src/client/services/create-page/use-create-template-page.ts
  11. 2 0
      apps/app/src/client/services/side-effects/drawio-modal-launcher-for-view.ts
  12. 3 0
      apps/app/src/client/services/side-effects/handsontable-modal-launcher-for-view.ts
  13. 21 10
      apps/app/src/components/Admin/Customize/ThemeColorBox.tsx
  14. 7 7
      apps/app/src/components/Admin/Security/LdapAuthTest.tsx
  15. 1 1
      apps/app/src/components/Admin/Security/SamlSecuritySettingContents.jsx
  16. 23 17
      apps/app/src/components/Admin/UserGroup/UserGroupDeleteModal.tsx
  17. 17 4
      apps/app/src/components/Admin/UserGroup/UserGroupPage.tsx
  18. 12 3
      apps/app/src/components/Admin/UserGroupDetail/UserGroupDetailPage.tsx
  19. 5 0
      apps/app/src/components/Common/PagePathHierarchicalLink/PagePathHierarchicalLink.module.scss
  20. 2 3
      apps/app/src/components/Common/PagePathHierarchicalLink/PagePathHierarchicalLink.tsx
  21. 2 2
      apps/app/src/components/Common/PagePathNav/PagePathNav.tsx
  22. 2 4
      apps/app/src/components/CustomNavigation/CustomNav.module.scss
  23. 1 1
      apps/app/src/components/CustomNavigation/CustomNav.tsx
  24. 0 19
      apps/app/src/components/Icons/CompressIcon.tsx
  25. 0 22
      apps/app/src/components/Icons/CreatePageIcon.tsx
  26. 0 19
      apps/app/src/components/Icons/ExpandIcon.tsx
  27. 3 21
      apps/app/src/components/Icons/FolderIcon.tsx
  28. 0 16
      apps/app/src/components/Icons/FolderPlusIcon.tsx
  29. 0 13
      apps/app/src/components/Icons/KeyboardReturnEnterIcon.tsx
  30. 0 20
      apps/app/src/components/Icons/MoonIcon.jsx
  31. 0 16
      apps/app/src/components/Icons/PagePreviewIcon.jsx
  32. 0 15
      apps/app/src/components/Icons/ReturnTopIcon.tsx
  33. 0 28
      apps/app/src/components/Icons/SunIcon.jsx
  34. 7 0
      apps/app/src/components/LoadingSpinner.jsx
  35. 39 0
      apps/app/src/components/LoadingSpinner.module.scss
  36. 11 5
      apps/app/src/components/LoginForm.tsx
  37. 2 2
      apps/app/src/components/Me/ApiSettings.tsx
  38. 1 1
      apps/app/src/components/Me/AssociateModal.tsx
  39. 2 2
      apps/app/src/components/Me/ColorModeSettings.tsx
  40. 10 10
      apps/app/src/components/Me/ExternalAccountLinkedMe.jsx
  41. 3 4
      apps/app/src/components/Me/InAppNotificationSettings.tsx
  42. 6 6
      apps/app/src/components/Me/PasswordSettings.jsx
  43. 2 2
      apps/app/src/components/Me/ProfileImageSettings.tsx
  44. 9 9
      apps/app/src/components/Me/QuestionnaireSettings.tsx
  45. 2 2
      apps/app/src/components/Me/UISettings.tsx
  46. 4 0
      apps/app/src/components/Navbar/GrowiContextualSubNavigation.module.scss
  47. 3 1
      apps/app/src/components/Navbar/PageEditorModeManager.tsx
  48. 3 1
      apps/app/src/components/PageComment/CommentEditor.tsx
  49. 1 1
      apps/app/src/components/PageControls/_button-styles.scss
  50. 7 2
      apps/app/src/components/PageCreateModal.tsx
  51. 1 1
      apps/app/src/components/PageEditor/Cheatsheet.tsx
  52. 0 0
      apps/app/src/components/PageEditor/EditorNavbar/EditingUserList.module.scss
  53. 57 0
      apps/app/src/components/PageEditor/EditorNavbar/EditingUserList.tsx
  54. 3 0
      apps/app/src/components/PageEditor/EditorNavbar/EditorNavbar.module.scss
  55. 22 0
      apps/app/src/components/PageEditor/EditorNavbar/EditorNavbar.tsx
  56. 1 0
      apps/app/src/components/PageEditor/EditorNavbar/index.ts
  57. 1 2
      apps/app/src/components/PageEditor/LinkEditModal.tsx
  58. 19 15
      apps/app/src/components/PageEditor/OptionsSelector.tsx
  59. 11 12
      apps/app/src/components/PageEditor/PageEditor.tsx
  60. 0 53
      apps/app/src/components/PageHeader/EditingUserList.tsx
  61. 1 12
      apps/app/src/components/PageHeader/PageHeader.tsx
  62. 1 1
      apps/app/src/components/PageHeader/PagePathHeader.module.scss
  63. 1 0
      apps/app/src/components/PageHeader/index.ts
  64. 6 6
      apps/app/src/components/PageSideContents/PageAccessoriesControl.module.scss
  65. 1 1
      apps/app/src/components/PageSideContents/PageAccessoriesControl.tsx
  66. 9 5
      apps/app/src/components/PageSideContents/PageSideContents.tsx
  67. 2 2
      apps/app/src/components/PageTags/PageTags.tsx
  68. 1 1
      apps/app/src/components/PageTags/RenderTagLabels.tsx
  69. 1 1
      apps/app/src/components/PageTags/TagEditModal.tsx
  70. 9 8
      apps/app/src/components/PageTags/TagLabels.module.scss
  71. 23 0
      apps/app/src/components/PageTags/TagsInput.module.scss
  72. 12 2
      apps/app/src/components/PageTags/TagsInput.tsx
  73. 3 2
      apps/app/src/components/ReactMarkdownComponents/NextLink.tsx
  74. 1 2
      apps/app/src/components/ShortcutsModal.tsx
  75. 1 2
      apps/app/src/components/Sidebar/Bookmarks/BookmarkContents.tsx
  76. 2 1
      apps/app/src/components/Sidebar/Custom/CustomSidebarNotFound.tsx
  77. 8 1
      apps/app/src/components/Sidebar/PageCreateButton/hooks/use-create-new-page.ts
  78. 2 1
      apps/app/src/components/Sidebar/PageCreateButton/hooks/use-create-todays-memo.tsx
  79. 3 14
      apps/app/src/components/Sidebar/RecentChanges/RecentChangesSubstance.module.scss
  80. 6 4
      apps/app/src/components/Sidebar/RecentChanges/RecentChangesSubstance.tsx
  81. 33 0
      apps/app/src/components/Sidebar/SidebarNav/PersonalDropdown.module.scss
  82. 40 32
      apps/app/src/components/Sidebar/SidebarNav/PersonalDropdown.tsx
  83. 3 2
      apps/app/src/components/SlackNotification.tsx
  84. 7 3
      apps/app/src/components/TableOfContents.tsx
  85. 4 1
      apps/app/src/components/TreeItem/NewPageInput/use-new-page-input.tsx
  86. 10 10
      apps/app/src/components/UsersHomepageFooter.module.scss
  87. 5 15
      apps/app/src/components/UsersHomepageFooter.tsx
  88. 18 3
      apps/app/src/features/external-user-group/client/components/ExternalUserGroup/ExternalUserGroupManagement.tsx
  89. 8 6
      apps/app/src/features/external-user-group/server/routes/apiv3/external-user-group.ts
  90. 5 2
      apps/app/src/interfaces/apiv3/page.ts
  91. 0 11
      apps/app/src/interfaces/editor-settings.ts
  92. 3 1
      apps/app/src/interfaces/page.ts
  93. 1 1
      apps/app/src/pages/installer.page.tsx
  94. 1 1
      apps/app/src/pages/me/[[...path]].page.tsx
  95. 4 4
      apps/app/src/server/models/editor-settings.ts
  96. 9 2
      apps/app/src/server/models/obsolete-page.js
  97. 5 1
      apps/app/src/server/models/revision.js
  98. 4 2
      apps/app/src/server/routes/apiv3/page/create-page.ts
  99. 12 6
      apps/app/src/server/routes/apiv3/page/update-page.ts
  100. 8 6
      apps/app/src/server/routes/apiv3/user-group.js

+ 1 - 1
.github/workflows/ci-app-prod.yml

@@ -48,7 +48,7 @@ concurrency:
 
 jobs:
 
-  test-prod-node16:
+  test-prod-node18:
     uses: weseek/growi/.github/workflows/reusable-app-prod.yml@dev/7.0.x
     with:
       node-version: 18.x

+ 1 - 1
.mergify.yml

@@ -6,8 +6,8 @@ pull_request_rules:
       - check-success = "lint (20.x)"
       - check-success = "test (20.x)"
       - check-success = "launch-dev (20.x)"
-      - check-success = "test-prod-node16 / launch-prod"
       - check-success = "test-prod-node18 / launch-prod"
+      - check-success = "test-prod-node20 / launch-prod"
     actions:
       merge:
         method: merge

+ 29 - 1
CHANGELOG.md

@@ -1,9 +1,36 @@
 # Changelog
 
-## [Unreleased](https://github.com/weseek/growi/compare/v6.3.0...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v6.3.1...HEAD)
 
 *Please do not manually update this file. We've automated the process.*
 
+## [v6.3.1](https://github.com/weseek/growi/compare/v6.3.0...v6.3.1) - 2024-02-01
+
+### 💎 Features
+
+* feat: Normalize duplicated root pages to valid paths when server startup (#8414) @miya
+
+### 🚀 Improvement
+
+* imprv: Use unzip stream instead of unzipper (#8378) @ryu-sato
+* imprv: Allow plugin that contain slashes in the branch name to be installed (#8359) @ryu-sato
+
+### 🐛 Bug Fixes
+
+* fix: Page being able to delete completely when not allowed (#8374) @arafubeatbox
+* fix: Logs are not saved when viewing the page (#8406) @miya
+* fix: Preventing duplication of `/user/username` pages (#8413) @WNomunomu
+* fix: Non-admin user cannot rename pages v63x (#8410) @jam411
+* fix: Duplicate root pages are created unintentionally (#8404) @miya
+* fix: Configured auditlog environment variables are not reflected in the administration screen (#8383) @miya
+* fix: plugin is broken after unzipping (#8358) @ryu-sato
+* fix: Keycloak group sync config not loaded on sync execution (#8339) @arafubeatbox
+
+### 🧰 Maintenance
+
+* support: React Testing Library (#8393) @miya
+* ci(deps-dev): bump vite from 4.5.1 to 4.5.2 (#8392) @dependabot
+
 ## [v6.3.0](https://github.com/weseek/growi/compare/v6.2.5...v6.3.0) - 2023-12-14
 
 ### BREAKING CHANGES
@@ -50,6 +77,7 @@
 * imprv: Allow deletion of user homepage when the user is deleted (#8224) @jam411
 
 ### 🐛 Bug Fixes
+
 * fix: Certify shared page attachment middleware (6.2.x) (#8256) @yuki-takei
 
 ### 🧰 Maintenance

+ 0 - 4
apps/app/_obsolete/src/components/Navbar/GrowiNavbar.module.scss

@@ -62,10 +62,6 @@
     background: rgba(0, 0, 0, 0.2);
   }
 
-  .grw-email-sm {
-    font-size: 0.75em;
-  }
-
   .grw-notification-dropdown {
     .dropdown-menu {
       max-width: 70vw;

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

@@ -5,6 +5,7 @@ import React, {
   useEffect,
 } from 'react';
 
+import type { EditorSettings } from '@growi/editor';
 import Dropzone from 'react-dropzone';
 import { useTranslation } from 'react-i18next';
 import {
@@ -12,7 +13,6 @@ import {
 } from 'reactstrap';
 
 import { toastError, toastSuccess } from '~/client/util/toastr';
-import type { IEditorSettings } from '~/interfaces/editor-settings';
 import { useDefaultIndentSize } from '~/stores/context';
 import { useEditorSettings } from '~/stores/editor';
 import { useIsMobile } from '~/stores/ui';
@@ -36,7 +36,7 @@ export type EditorPropsType = {
   isUploadAllFileAllowed?: boolean,
   onChange?: (newValue: string, isClean?: boolean) => void,
   onUpload?: (file) => void,
-  editorSettings?: IEditorSettings,
+  editorSettings?: EditorSettings,
   indentSize?: number,
   onDragEnter?: (event: any) => void,
   onMarkdownHelpButtonClicked?: () => void,

+ 2 - 4
apps/app/_obsolete/src/components/Sidebar/AppearanceModeDropdown.tsx

@@ -10,10 +10,8 @@ import { useUserUISettings } from '~/client/services/user-ui-settings';
 import { usePreferDrawerModeByUser, usePreferDrawerModeOnEditByUser } from '~/stores/ui';
 import { Themes, useNextThemes } from '~/stores/use-next-themes';
 
-import MoonIcon from '../Icons/MoonIcon';
 import SidebarDockIcon from '../Icons/SidebarDockIcon';
 import SidebarDrawerIcon from '../Icons/SidebarDrawerIcon';
-import SunIcon from '../Icons/SunIcon';
 
 type AppearanceModeDropdownProps = {
   isAuthenticated: boolean,
@@ -132,7 +130,7 @@ export const AppearanceModeDropdown:FC<AppearanceModeDropdownProps> = (props: Ap
               <div className="justify-content-center">
                 <div className="col-auto d-flex align-items-center">
                   <IconWithTooltip id="iwt-light" label="Light" additionalClasses={useOsSettings ? 'grw-color-mode-icon-muted' : 'grw-color-mode-icon'}>
-                    <SunIcon />
+                  <span className="material-symbols-outlined">light_mode</span>
                   </IconWithTooltip>
                   <div className="form-check form-switch form-check-secondary ms-2">
                     <input
@@ -146,7 +144,7 @@ export const AppearanceModeDropdown:FC<AppearanceModeDropdownProps> = (props: Ap
                     <label className="form-label form-check-label" htmlFor="swUserPreference"></label>
                   </div>
                   <IconWithTooltip id="iwt-dark" label="Dark" additionalClasses={useOsSettings ? 'grw-color-mode-icon-muted' : 'grw-color-mode-icon'}>
-                    <MoonIcon />
+                  <span className="material-symbols-outlined">dark_mode</span>
                   </IconWithTooltip>
                 </div>
               </div>

+ 75 - 68
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",
@@ -112,7 +112,7 @@
   "Create under": "Create page under below:",
   "V5 Page Migration": "Convert To V5 Compatibility",
   "GROWI.5.0_new_schema": "GROWI.5.0 new schema",
-  "See_more_detail_on_new_schema": "See more detail on <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> ",
+  "See_more_detail_on_new_schema": "See more detail on <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> ",
   "external_account_management": "External Account Management",
   "UserGroup": "UserGroup",
   "Basic Settings": "Basic Settings",
@@ -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,10 +307,15 @@
       "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": {
+    "input_channels": "Input channels",
+    "theme": "Theme",
+    "keymap": "Keymap",
+    "indent": "Indent",
+    "editor_config": "Editor Config",
     "Show active line": "Show active line",
     "auto_format_table": "Auto format table",
     "overwrite_scopes": "{{operation}} and Overwrite scopes of all descendants",
@@ -337,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": {
@@ -376,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.",
@@ -436,8 +441,8 @@
     "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",
@@ -466,7 +471,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": {
@@ -532,13 +537,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",
@@ -554,7 +559,7 @@
     "alert_desc1": "On this page, you can select pages with the checkbox and batch convert to the new v5 compatible format from the \"Bulk operation\" button at the top of the screen.",
     "nopages_title": "Congratulations. Ready to use GROWI v5!",
     "nopages_desc1": "Now all the pages you can manage seem to be in v5 compatible format.",
-    "detail_info": "See the detail information from <a href='https://docs.growi.org/en/admin-guide/upgrading/50x.html' target='_blank' class='alert-link'>Upgrading GROWI to v5.0.x <i class='icon-share-alt'></i></a>.",
+    "detail_info": "See the detail information from <a href='https://docs.growi.org/en/admin-guide/upgrading/50x.html' target='_blank' class='alert-link'>Upgrading GROWI to v5.0.x <span className='growi-custom-icons'>external_link</span></a>.",
     "modal": {
       "title": "Convert to new v5 compatible format",
       "converting_pages": "Converting pages",
@@ -585,7 +590,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": {
@@ -612,21 +617,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.",
@@ -643,20 +648,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",
@@ -673,7 +678,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",
@@ -703,7 +708,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",
@@ -715,10 +720,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",
@@ -765,15 +770,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": {
@@ -791,7 +796,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": {
@@ -828,15 +833,17 @@
     "select_page_location": "Select page location"
   },
   "wip_page": {
-    "save_as_wip": "Save as WIP (Currently drafting)",
+    "save_as_wip": "Save as WIP (still being written)",
     "success_save_as_wip": "Successfully saved as a WIP page",
     "fail_save_as_wip": "Failed to save as a WIP page",
-    "alert": "This page is a work in progress",
+    "alert": "This page is still being written",
     "publish_page": "Publish page",
     "success_publish_page": "Page has been published",
     "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"
   }
 }

+ 73 - 66
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": "移動/名前変更",
@@ -111,7 +111,7 @@
   "Create under": "ページを以下に作成",
   "V5 Page Migration": "V5 互換形式 への変換",
   "GROWI.5.0_new_schema": "GROWI.5.0における新スキーマについて",
-  "See_more_detail_on_new_schema": "詳しくは<a href='https://docs.growi.org/ja/admin-guide/upgrading/50x.html#新しい-v5-互換形式について' target='_blank'>{{title}}</a><i class='icon-share-alt'></i>を参照ください。",
+  "See_more_detail_on_new_schema": "詳しくは<a href='https://docs.growi.org/ja/admin-guide/upgrading/50x.html#新しい-v5-互換形式について' target='_blank'>{{title}}</a><span className='growi-custom-icons'>external_link</span>を参照ください。",
   "external_account_management": "外部アカウント管理",
   "UserGroup": "グループ",
   "Basic Settings": "基本設定",
@@ -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}}年以上が経過しています。",
@@ -344,6 +344,11 @@
     }
   },
   "page_edit": {
+    "input_channels": "チャンネル名",
+    "theme": "テーマ",
+    "keymap": "キーマップ",
+    "indent": "インデント",
+    "editor_config": "エディタ設定",
     "Show active line": "アクティブ行をハイライト",
     "auto_format_table": "表の自動整形",
     "overwrite_scopes": "{{operation}}と同時に全ての配下ページのスコープを上書き",
@@ -370,8 +375,8 @@
     "comparing_source": "ソース",
     "comparing_target": "ターゲット",
     "comparing_revisions": "差分を比較する",
-    "compare_latest":"最新と比較",
-    "compare_previous":"1つ前のバージョンと比較"
+    "compare_latest": "最新と比較",
+    "compare_previous": "1つ前のバージョンと比較"
   },
   "modal_rename": {
     "label": {
@@ -409,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": "権限がないため、いくつかのページは削除できません",
@@ -469,8 +474,8 @@
     "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": "最新の本文",
@@ -499,7 +504,7 @@
     "issue_share_link": "共有リンクを作成しました",
     "remove_share_link": "共有リンクを{{count}}件削除しました",
     "switch_disable_link_sharing_success": "共有リンクの設定を変更しました",
-    "failed_to_reset_password":"パスワードのリセットに失敗しました",
+    "failed_to_reset_password": "パスワードのリセットに失敗しました",
     "save_succeeded": "保存に成功しました"
   },
   "template": {
@@ -566,12 +571,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": "作成日時",
@@ -587,7 +592,7 @@
     "alert_desc1": "このページでは、チェックボックスでページを選択し、画面上部の「一括操作」ボタンから新しい v5 互換形式に一括変換できます。",
     "nopages_title": "おめでとうございます。GROWI v5 を使う準備が完了しました!",
     "nopages_desc1": "今あなたが管理可能なページはすべて v5 互換形式になっているようです。",
-    "detail_info": "詳しくは <a href='https://docs.growi.org/ja/admin-guide/upgrading/50x.html' target='_blank' class='alert-link'>GROWI v5.0.x へのアップグレード  <i class='icon-share-alt'></i></a> を参照ください。",
+    "detail_info": "詳しくは <a href='https://docs.growi.org/ja/admin-guide/upgrading/50x.html' target='_blank' class='alert-link'>GROWI v5.0.x へのアップグレード <span className='growi-custom-icons'>external_link</span></a> を参照ください。",
     "modal": {
       "title": "新しい v5 互換形式への変換",
       "converting_pages": "以下のページを変換します",
@@ -618,7 +623,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": {
@@ -644,23 +649,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": "ユーザー名に不正な文字が含まれています.",
@@ -676,20 +681,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": "ログイン画面に戻る",
@@ -702,11 +707,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": "リセット",
@@ -736,7 +741,7 @@
       "6": "肌の色が濃い"
     }
   },
-  "maintenance_mode":{
+  "maintenance_mode": {
     "maintenance_mode": "メンテナンスモード",
     "growi_is_under_maintenance": "GROWI はメンテナンス中です。終了するまでお待ちください",
     "admin_page": "管理画面へ",
@@ -748,10 +753,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": "グループを選ぶ",
@@ -798,15 +803,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": {
@@ -824,7 +829,7 @@
     "root": "root (default)"
   },
   "v5_page_migration": {
-    "page_tree_not_avaliable" : "Page Tree 機能は現在使用できません。",
+    "page_tree_not_avaliable": "Page Tree 機能は現在使用できません。",
     "go_to_settings": "設定する"
   },
   "questionnaire": {
@@ -861,15 +866,17 @@
     "select_page_location": "ページの場所を選択"
   },
   "wip_page": {
-    "save_as_wip": "WIP (執筆中) として保存",
+    "save_as_wip": "WIP (執筆中) として保存",
     "success_save_as_wip": "WIP ページとして保存しました",
     "fail_save_as_wip": "WIP ページとして保存できませんでした",
-    "alert": "このページは作業途中です",
+    "alert": "このページは執筆途中です",
     "publish_page": "WIP を解除",
     "success_publish_page": "WIP を解除しました",
     "fail_publish_page": "WIP を解除できませんでした"
   },
   "sidebar_header": {
-    "show_wip_page": "WIP を表示"
+    "show_wip_page": "WIP を表示",
+    "size_s": "サイズ: S",
+    "size_l": "サイズ: L"
   }
 }

+ 376 - 369
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,76 +73,76 @@
   "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> <i class='icon-share-alt'></i> ",
+  "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> ",
 	"Markdown Settings": "Markdown设置",
 	"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,48 +259,53 @@
   "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"
 		}
 	},
 	"page_edit": {
+    "input_channels": "频道名",
+    "theme": "主题",
+    "keymap": "键表",
+    "indent": "缩进",
+    "editor_config": "编辑器配置",
 		"Show active line": "显示活动行",
 		"auto_format_table": "自动格式化表格",
 		"overwrite_scopes": "{{operation}和覆盖所有子体的作用域",
@@ -313,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": "您无权永久删除该页面"
   },
@@ -327,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",
@@ -342,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",
@@ -389,44 +394,44 @@
     }
   },
   "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": "最新页面正文",
@@ -448,64 +453,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."
@@ -514,7 +519,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",
@@ -522,42 +527,42 @@
     "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!",
     "nopages_desc1": "现在你能管理的所有页面似乎都是v5兼容的格式。",
-    "detail_info": "请参见 <a href='https://docs.growi.org/en/admin-guide/upgrading/50x.html' target='_blank' class='alert-link'>升级GROWI到v5.0.x <i class='icon-share-alt'></i></a>.的详细内容。",
+    "detail_info": "请参见 <a href='https://docs.growi.org/en/admin-guide/upgrading/50x.html' target='_blank' class='alert-link'>升级GROWI到v5.0.x <span className='growi-custom-icons'>external_link</span></a>.的详细内容。",
     "modal": {
       "title": "转换为新的v5兼容格式",
       "converting_pages": "转换页面",
@@ -583,14 +588,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": "创建账户",
@@ -602,35 +607,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": "用户名有无效字符",
@@ -645,21 +650,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": "返回登录",
@@ -672,11 +677,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": "重置",
@@ -706,7 +711,7 @@
       "6": "深色肤色"
     }
   },
-  "maintenance_mode":{
+  "maintenance_mode": {
     "maintenance_mode": "维护模式",
     "growi_is_under_maintenance": "GROWI正在进行维护。请等待,直到它结束。",
     "admin_page": "管理员页",
@@ -718,10 +723,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": "选择组别",
@@ -768,9 +773,9 @@
       }
     }
   },
-  "page_operation":{
+  "page_operation": {
     "paths_recovered": "成功恢复了页面路径",
-    "path_recovery_failed":"路径恢复失败"
+    "path_recovery_failed": "路径恢复失败"
   },
   "footer": {
     "bookmarks": "书签",
@@ -831,15 +836,17 @@
     "select_page_location": "选择页面位置"
   },
   "wip_page": {
-    "save_as_wip": "保存为 WIP(书面)",
+    "save_as_wip": "保存为 WIP(仍在撰写中)",
     "success_save_as_wip": "成功保存为 WIP 页面",
     "fail_save_as_wip": "保存为 WIP 页失败",
-    "alert": "本页面正在制作中",
+    "alert": "本页仍在编写中",
     "publish_page": "发布 WIP",
     "success_publish_page": "WIP 已停用",
     "fail_publish_page": "无法停用 WIP"
   },
   "sidebar_header": {
-    "show_wip_page": "显示 WIP"
+    "show_wip_page": "显示 WIP",
+    "size_s": "尺寸: S",
+    "size_l": "尺寸: L"
   }
 }

+ 3 - 1
apps/app/src/client/services/create-page/use-create-template-page.ts

@@ -1,11 +1,13 @@
 import { useCallback } from 'react';
 
+import { Origin } from '@growi/core';
 import { isCreatablePage } from '@growi/core/dist/utils/page-path-utils';
 import { normalizePath } from '@growi/core/dist/utils/path-utils';
 
 import type { LabelType } from '~/interfaces/template';
 import { useCurrentPagePath } from '~/stores/page';
 
+
 import { useCreatePageAndTransit } from './use-create-page-and-transit';
 
 type UseCreateTemplatePage = () => {
@@ -25,7 +27,7 @@ export const useCreateTemplatePage: UseCreateTemplatePage = () => {
     if (isLoadingPagePath || !isCreatable) return;
 
     return createAndTransit(
-      { path: normalizePath(`${currentPagePath}/${label}`), wip: false },
+      { path: normalizePath(`${currentPagePath}/${label}`), wip: false, origin: Origin.View },
       { shouldCheckPageExists: true },
     );
   }, [currentPagePath, isCreatable, isLoadingPagePath, createAndTransit]);

+ 2 - 0
apps/app/src/client/services/side-effects/drawio-modal-launcher-for-view.ts

@@ -2,6 +2,7 @@ import { useCallback, useEffect } from 'react';
 
 import type EventEmitter from 'events';
 
+import { Origin } from '@growi/core';
 import type { DrawioEditByViewerProps } from '@growi/remark-drawio';
 
 import { replaceDrawioInMarkdown } from '~/components/Page/markdown-drawio-util-for-view';
@@ -47,6 +48,7 @@ export const useDrawioModalLauncherForView = (opts?: {
         pageId: currentPage._id,
         revisionId: currentRevisionId,
         body: newMarkdown,
+        origin: Origin.View,
       });
 
       opts?.onSaveSuccess?.();

+ 3 - 0
apps/app/src/client/services/side-effects/handsontable-modal-launcher-for-view.ts

@@ -2,6 +2,8 @@ import { useCallback, useEffect } from 'react';
 
 import type EventEmitter from 'events';
 
+import { Origin } from '@growi/core';
+
 import type MarkdownTable from '~/client/models/MarkdownTable';
 import { getMarkdownTableFromLine, replaceMarkdownTableInMarkdown } from '~/components/Page/markdown-table-util-for-view';
 import { useShareLinkId } from '~/stores/context';
@@ -46,6 +48,7 @@ export const useHandsontableModalLauncherForView = (opts?: {
         pageId: currentPage._id,
         revisionId: currentRevisionId,
         body: newMarkdown,
+        origin: Origin.View,
       });
 
       opts?.onSaveSuccess?.();

+ 21 - 10
apps/app/src/components/Admin/Customize/ThemeColorBox.tsx

@@ -15,26 +15,37 @@ export const ThemeColorBox = (props: Props): JSX.Element => {
     isSelected, metadata, onSelected,
   } = props;
   const {
-    name, bg, topbar, sidebar, accent, isPresetTheme,
+    name, lightBg, darkBg, lightSidebar, darkSidebar, lightIcon, darkIcon, createBtn, isPresetTheme,
   } = metadata;
 
   return (
+    // TODO: Display a primary color border when icon is selected
     <div
       id={`theme-option-${name}`}
       className={`theme-option-container d-flex flex-column align-items-center ${isSelected ? 'active' : ''}`}
       onClick={onSelected}
     >
-      <a id={name} role="button" className={`m-0 ${name} theme-button`}>
-        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
-          <g>
-            <path d="M -1 -1 L65 -1 L65 65 L-1 65 L-1 -1 Z" fill={bg}></path>
-            <path d="M -1 -1 L65 -1 L65 15 L-1 15 L-1 -1 Z" fill={topbar}></path>
-            <path d="M -1 15 L15 15 L15 65 L-1 65 L-1 15 Z" fill={sidebar}></path>
-            <path d="M 65 45 L65 65 L45 65 L65 45 Z" fill={accent}></path>
-          </g>
+      <a id={name} role="button" className={`m-0 rounded ${name} theme-button`}>
+        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" className="rounded">
+          <path d="M32.5,0V36.364L64,20.437V0Z" fill={lightBg} />
+          <path d="M32.5,36.364V64H64V20.438Z" fill={darkBg} />
+          <path
+            d="M4.077,20.648,10.164,10.1H22.338l6.087,10.544L22.338,31.19H10.164ZM0,0V52.8l6.436-3.255v-1.8H10L17.189,44.1H6.436V42.044H21.267L32.5,36.364V0Z"
+            fill={lightSidebar}
+          />
+          <path
+            d="M6.436,53.44H26.065V55.5H6.436Zm14.831-11.4h4.8v2.061H17.189L10,47.743H26.065V49.8l-19.629,0v-.259L0,52.8V64H32.5V36.364Z"
+            fill={darkSidebar}
+          />
+          <path d="M22.338,31.19l6.087-10.543L22.338,10.1H10.163L4.077,20.647,10.163,31.19Z" fill={createBtn} />
+          <path d="M6.436,49.543,10,47.742H6.436Z" fill={lightIcon} />
+          <path d="M6.436,44.106H17.189l4.078-2.062H6.436Z" fill={lightIcon} />
+          <path d="M6.436,49.8l19.629,0V47.742H10l-3.561,1.8Z" fill={darkIcon} />
+          <path d="M26.065,44.106V42.044h-4.8L17.19,44.106Z" fill={darkIcon} />
+          <rect width="19.629" height="2.062" transform="translate(6.436 53.439)" fill={darkIcon} />
         </svg>
       </a>
-      <span className="theme-option-name"><b>{ name }</b></span>
+      <span className="theme-option-name mt-2"><b>{ name }</b></span>
       { !isPresetTheme && <span className="theme-option-badge badge bg-primary mt-1">Plugin</span> }
     </div>
   );

+ 7 - 7
apps/app/src/components/Admin/Security/LdapAuthTest.tsx

@@ -4,7 +4,7 @@ import { useTranslation } from 'next-i18next';
 
 import { apiPost } from '~/client/util/apiv1-client';
 import { toastSuccess, toastError } from '~/client/util/toastr';
-import { IResTestLdap } from '~/interfaces/ldap';
+import type { IResTestLdap } from '~/interfaces/ldap';
 import loggerFactory from '~/utils/logger';
 
 const logger = loggerFactory('growi:security:AdminLdapSecurityContainer');
@@ -89,8 +89,8 @@ export const LdapAuthTest = (props: LdapAuthTestProps): JSX.Element => {
     <React.Fragment>
       {successMessage !== '' && <div className="alert alert-success">{successMessage}</div>}
       {errorMessage !== '' && <div className="alert alert-warning">{errorMessage}</div>}
-      <div className="row">
-        <label htmlFor="username" className="col-3 col-form-label">{t('username')}</label>
+      <div className="row mt-3">
+        <label htmlFor="username" className="col-3 col-form-label text-end">{t('username')}</label>
         <div className="col-6">
           <input
             className="form-control"
@@ -101,8 +101,8 @@ export const LdapAuthTest = (props: LdapAuthTestProps): JSX.Element => {
           />
         </div>
       </div>
-      <div className="row">
-        <label htmlFor="password" className="col-3 col-form-label">{t('Password')}</label>
+      <div className="row mt-3">
+        <label htmlFor="password" className="col-3 col-form-label text-end">{t('Password')}</label>
         <div className="col-6">
           <input
             className="form-control"
@@ -115,12 +115,12 @@ export const LdapAuthTest = (props: LdapAuthTestProps): JSX.Element => {
         </div>
       </div>
 
-      <div>
+      <div className="mt-4">
         <label className="form-label"><h5>Logs</h5></label>
         <textarea id="taLogs" className="col form-control" rows={4} value={logs} readOnly />
       </div>
 
-      <div>
+      <div className="mt-4">
         <button type="button" className="btn btn-outline-secondary offset-5 col-2" onClick={testLdapCredentials}>Test</button>
       </div>
     </React.Fragment>

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

@@ -471,7 +471,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                           target="_blank"
                           rel="noreferer noreferrer"
                         >
-                          Apache Lucene - Query Parser Syntax <i className="icon-share-alt"></i>
+                          Apache Lucene - Query Parser Syntax <span className="growi-custom-icons">external_link</span>
                         </a>.
                       </p>
                       <div className="accordion" id="accordionId">

+ 23 - 17
apps/app/src/components/Admin/UserGroup/UserGroupDeleteModal.tsx

@@ -1,7 +1,9 @@
 import type { FC } from 'react';
 import React, { useCallback, useState, useMemo } from 'react';
 
-import type { IUserGroupHasId } from '@growi/core';
+import {
+  getIdForRef, isPopulated, type IGrantedGroup, type IUserGroupHasId,
+} from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import {
   Modal, ModalHeader, ModalBody, ModalFooter,
@@ -18,9 +20,9 @@ import { PageActionOnGroupDelete } from '~/interfaces/user-group';
  * @extends {React.Component}
  */
 type Props = {
-  userGroups: IUserGroupHasId[],
+  userGroups: IGrantedGroup[],
   deleteUserGroup?: IUserGroupHasId,
-  onDelete?: (deleteGroupId: string, actionName: PageActionOnGroupDelete, transferToUserGroupId: string) => Promise<void> | void,
+  onDelete?: (deleteGroupId: string, actionName: PageActionOnGroupDelete, transferToUserGroup: IGrantedGroup | null) => Promise<void> | void,
   isShow: boolean,
   onHide?: () => Promise<void> | void,
 };
@@ -71,14 +73,14 @@ export const UserGroupDeleteModal: FC<Props> = (props: Props) => {
    * State
    */
   const [actionName, setActionName] = useState<PageActionOnGroupDelete | null>(null);
-  const [transferToUserGroupId, setTransferToUserGroupId] = useState<string>('');
+  const [transferToUserGroup, setTransferToUserGroup] = useState<IGrantedGroup | null>(null);
 
   /*
    * Function
    */
   const resetStates = useCallback(() => {
     setActionName(null);
-    setTransferToUserGroupId('');
+    setTransferToUserGroup(null);
   }, []);
 
   const toggleHandler = useCallback(() => {
@@ -97,8 +99,9 @@ export const UserGroupDeleteModal: FC<Props> = (props: Props) => {
 
   const handleGroupChange = useCallback((e) => {
     const transferToUserGroupId = e.target.value;
-    setTransferToUserGroupId(transferToUserGroupId);
-  }, []);
+    const selectedGroup = userGroups.find(group => getIdForRef(group.item) === transferToUserGroupId) ?? null;
+    setTransferToUserGroup(selectedGroup);
+  }, [userGroups]);
 
   const handleSubmit = useCallback((e) => {
     if (onDelete == null || deleteUserGroup == null || actionName == null) {
@@ -110,9 +113,9 @@ export const UserGroupDeleteModal: FC<Props> = (props: Props) => {
     onDelete(
       deleteUserGroup._id,
       actionName,
-      transferToUserGroupId,
+      transferToUserGroup,
     );
-  }, [onDelete, deleteUserGroup, actionName, transferToUserGroupId]);
+  }, [onDelete, deleteUserGroup, actionName, transferToUserGroup]);
 
   const renderPageActionSelector = useCallback(() => {
     const options = availableOptions.map((opt) => {
@@ -139,28 +142,31 @@ export const UserGroupDeleteModal: FC<Props> = (props: Props) => {
     }
 
     const groups = userGroups.filter((group) => {
-      return group._id !== deleteUserGroup._id;
+      return getIdForRef(group.item) !== deleteUserGroup._id;
     });
 
     const options = groups.map((group) => {
-      return <option key={group._id} value={group._id}>{group.name}</option>;
-    });
+      const groupId = getIdForRef(group.item);
+      const groupName = isPopulated(group.item) ? group.item.name : null;
+      return { id: groupId, name: groupName };
+    }).filter(obj => obj.name != null)
+      .map(obj => <option key={obj.id} value={obj.id}>{obj.name}</option>);
 
     const defaultOptionText = groups.length === 0 ? t('admin:user_group_management.delete_modal.no_groups')
       : t('admin:user_group_management.delete_modal.select_group');
 
     return (
       <select
-        name="transferToUserGroupId"
+        name="transferToUserGroup"
         className={`form-control ${actionName === PageActionOnGroupDelete.transfer ? '' : 'd-none'}`}
-        value={transferToUserGroupId}
+        value={transferToUserGroup != null ? getIdForRef(transferToUserGroup.item) : ''}
         onChange={handleGroupChange}
       >
         <option value="" disabled>{defaultOptionText}</option>
         {options}
       </select>
     );
-  }, [deleteUserGroup, userGroups, t, actionName, transferToUserGroupId, handleGroupChange]);
+  }, [deleteUserGroup, userGroups, t, actionName, transferToUserGroup, handleGroupChange]);
 
   const validateForm = useCallback(() => {
     let isValid = true;
@@ -169,11 +175,11 @@ export const UserGroupDeleteModal: FC<Props> = (props: Props) => {
       isValid = false;
     }
     else if (actionName === PageActionOnGroupDelete.transfer) {
-      isValid = transferToUserGroupId !== '';
+      isValid = transferToUserGroup != null;
     }
 
     return isValid;
-  }, [actionName, transferToUserGroupId]);
+  }, [actionName, transferToUserGroup]);
 
   return (
     <Modal className="modal-md" isOpen={props.isShow} toggle={toggleHandler}>

+ 17 - 4
apps/app/src/components/Admin/UserGroup/UserGroupPage.tsx

@@ -1,16 +1,19 @@
 import type { FC } from 'react';
 import React, { useState, useCallback } from 'react';
 
-import type { IUserGroup, IUserGroupHasId } from '@growi/core';
+import {
+  GroupType, getIdForRef, type IGrantedGroup, type IUserGroup, type IUserGroupHasId,
+} from '@growi/core';
 import dynamic from 'next/dynamic';
 import { useTranslation } from 'react-i18next';
 
 import { apiv3Delete, apiv3Post, apiv3Put } from '~/client/util/apiv3-client';
 import { toastSuccess, toastError } from '~/client/util/toastr';
 import { ExternalGroupManagement } from '~/features/external-user-group/client/components/ExternalUserGroup/ExternalUserGroupManagement';
+import { useSWRxExternalUserGroupList } from '~/features/external-user-group/client/stores/external-user-group';
+import type { PageActionOnGroupDelete } from '~/interfaces/user-group';
 import { useIsAclEnabled } from '~/stores/context';
 import { useSWRxUserGroupList, useSWRxChildUserGroupList, useSWRxUserGroupRelationList } from '~/stores/user-group';
-import { PageActionOnGroupDelete } from '~/interfaces/user-group';
 
 
 const UserGroupDeleteModal = dynamic(() => import('./UserGroupDeleteModal').then(mod => mod.UserGroupDeleteModal), { ssr: false });
@@ -26,7 +29,14 @@ export const UserGroupPage: FC = () => {
    * Fetch
    */
   const { data: userGroupList, mutate: mutateUserGroups } = useSWRxUserGroupList();
+  const { data: externalUserGroupList } = useSWRxExternalUserGroupList();
   const userGroups = userGroupList != null ? userGroupList : [];
+  const userGroupsForDeleteModal: IGrantedGroup[] = userGroups.map((group) => {
+    return { item: group, type: GroupType.userGroup };
+  });
+  const externalUserGroupsForDeleteModal: IGrantedGroup[] = externalUserGroupList != null ? externalUserGroupList.map((group) => {
+    return { item: group, type: GroupType.externalUserGroup };
+  }) : [];
   const userGroupIds = userGroups.map(group => group._id);
 
   const { data: userGroupRelationList } = useSWRxUserGroupRelationList(userGroupIds);
@@ -128,11 +138,14 @@ export const UserGroupPage: FC = () => {
     }
   }, [t, mutateUserGroups, hideUpdateModal]);
 
-  const deleteUserGroupById = useCallback(async(deleteGroupId: string, actionName: PageActionOnGroupDelete, transferToUserGroupId: string) => {
+  const deleteUserGroupById = useCallback(async(deleteGroupId: string, actionName: PageActionOnGroupDelete, transferToUserGroup: IGrantedGroup | null) => {
+    const transferToUserGroupId = transferToUserGroup != null ? getIdForRef(transferToUserGroup.item) : null;
+    const transferToUserGroupType = transferToUserGroup != null ? transferToUserGroup.type : null;
     try {
       await apiv3Delete(`/user-groups/${deleteGroupId}`, {
         actionName,
         transferToUserGroupId,
+        transferToUserGroupType,
       });
 
       // sync
@@ -189,7 +202,7 @@ export const UserGroupPage: FC = () => {
       />
 
       <UserGroupDeleteModal
-        userGroups={userGroups}
+        userGroups={userGroupsForDeleteModal.concat(externalUserGroupsForDeleteModal)}
         deleteUserGroup={selectedUserGroup}
         onDelete={deleteUserGroupById}
         isShow={isDeleteModalShown}

+ 12 - 3
apps/app/src/components/Admin/UserGroupDetail/UserGroupDetailPage.tsx

@@ -2,7 +2,9 @@ import React, {
   useState, useCallback, useEffect, useMemo,
 } from 'react';
 
-import type { IUserGroup, IUserGroupHasId } from '@growi/core';
+import {
+  GroupType, getIdForRef, type IGrantedGroup, type IUserGroup, type IUserGroupHasId,
+} from '@growi/core';
 import { objectIdUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
@@ -85,6 +87,10 @@ const UserGroupDetailPage = (props: Props): JSX.Element => {
 
   const { data: childUserGroupsList, mutate: mutateChildUserGroups, updateChild } = useChildUserGroupList(currentUserGroupId, isExternalGroup);
   const childUserGroups = childUserGroupsList != null ? childUserGroupsList.childUserGroups : [];
+  const childUserGroupsForDeleteModal: IGrantedGroup[] = childUserGroups.map((group) => {
+    const groupType = isExternalGroup ? GroupType.externalUserGroup : GroupType.userGroup;
+    return { item: group, type: groupType };
+  });
   const grandChildUserGroups = childUserGroupsList != null ? childUserGroupsList.grandChildUserGroups : [];
   const childUserGroupIds = childUserGroups.map(group => group._id);
 
@@ -297,12 +303,15 @@ const UserGroupDetailPage = (props: Props): JSX.Element => {
     setDeleteModalShown(false);
   }, [setSelectedUserGroup, setDeleteModalShown]);
 
-  const deleteChildUserGroupById = useCallback(async(deleteGroupId: string, actionName: PageActionOnGroupDelete, transferToUserGroupId: string) => {
+  const deleteChildUserGroupById = useCallback(async(deleteGroupId: string, actionName: PageActionOnGroupDelete, transferToUserGroup: IGrantedGroup | null) => {
     const url = isExternalGroup ? `/external-user-groups/${deleteGroupId}` : `/user-groups/${deleteGroupId}`;
+    const transferToUserGroupId = transferToUserGroup != null ? getIdForRef(transferToUserGroup.item) : null;
+    const transferToUserGroupType = transferToUserGroup != null ? transferToUserGroup.type : null;
     try {
       const res = await apiv3Delete(url, {
         actionName,
         transferToUserGroupId,
+        transferToUserGroupType,
       });
 
       // sync
@@ -449,7 +458,7 @@ const UserGroupDetailPage = (props: Props): JSX.Element => {
       />
 
       <UserGroupDeleteModal
-        userGroups={childUserGroups}
+        userGroups={childUserGroupsForDeleteModal}
         deleteUserGroup={selectedUserGroup}
         onDelete={deleteChildUserGroupById}
         isShow={isDeleteModalShown}

+ 5 - 0
apps/app/src/components/Common/PagePathHierarchicalLink/PagePathHierarchicalLink.module.scss

@@ -2,3 +2,8 @@
   margin-right: 0.2em;
   margin-left: 0.2em;
 }
+
+.material-symbols-outlined {
+  font-size: 1em;
+  line-height: inherit;
+}

+ 2 - 3
apps/app/src/components/Common/PagePathHierarchicalLink/PagePathHierarchicalLink.tsx

@@ -41,7 +41,7 @@ export const PagePathHierarchicalLink = memo((props: PagePathHierarchicalLinkPro
         <RootElm>
           <span className="path-segment">
             <Link href="/trash" prefetch={false}>
-              <span className="material-symbols-outlined">delete</span>
+              <span className={`material-symbols-outlined ${styles['material-symbols-outlined']}`}>delete</span>
             </Link>
           </span>
           <span className={`separator ${styles.separator}`}><a href="/">/</a></span>
@@ -51,8 +51,7 @@ export const PagePathHierarchicalLink = memo((props: PagePathHierarchicalLinkPro
         <RootElm>
           <span className="path-segment">
             <Link href="/" prefetch={false}>
-              {/* TODO: Size adjust */}
-              <span className="material-symbols-outlined">home</span>
+              <span className={`material-symbols-outlined ${styles['material-symbols-outlined']}`}>home</span>
               <span className={`separator ${styles.separator}`}>/</span>
             </Link>
           </span>

+ 2 - 2
apps/app/src/components/Common/PagePathNav/PagePathNav.tsx

@@ -72,10 +72,10 @@ export const PagePathNav: FC<Props> = (props: Props) => {
     const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
     const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
     formerLink = (
-      <>
+      <div className="fs-5">
         <PagePathHierarchicalLink linkedPagePath={linkedPagePathFormer} isInTrash={isInTrash} />
         <Separator />
-      </>
+      </div>
     );
     latterLink = (
       <>

+ 2 - 4
apps/app/src/components/CustomNavigation/CustomNav.module.scss

@@ -1,3 +1,5 @@
+@use '@growi/core/scss/bootstrap/init' as bs;
+
 .grw-custom-nav-tab :global {
   .nav-title {
     flex-wrap: nowrap;
@@ -13,8 +15,4 @@
     transition: 0.3s ease-in-out;
   }
 
-  .material-symbols-outlined {
-    margin-right: 6px;
-    font-size: 18px;
-  }
 }

+ 1 - 1
apps/app/src/components/CustomNavigation/CustomNav.tsx

@@ -183,7 +183,7 @@ export const CustomNavTab = (props: CustomNavTabProps): JSX.Element => {
                 className={`p-0 ${isActive ? 'active' : inactiveClassnames.join(' ')}`}
               >
                 <NavLink type="button" key={key} innerRef={elm => registerNavLink(key, elm)} disabled={!isLinkEnabled} onClick={() => navLinkClickHandler(key)}>
-                  { Icon != null && <Icon /> } {i18n}
+                  { Icon != null && <span className="me-1"><Icon /></span> } {i18n}
                 </NavLink>
               </NavItem>
             );

+ 0 - 19
apps/app/src/components/Icons/CompressIcon.tsx

@@ -1,19 +0,0 @@
-import React from 'react';
-
-export const CompressIcon = ():JSX.Element => {
-  return (
-    <svg
-      xmlns="http://www.w3.org/2000/svg"
-      width="18"
-      height="18"
-      viewBox="0 0 45 45"
-    >
-      <path
-        fill="currentColor"
-        d="M22.45 44v-7.9l-3.85 3.8-2.1-2.1 7.45-7.4 7.35 7.4-2.1
-            2.1-3.75-3.8V44ZM8.05 27.5v-3H40v3Zm0-6.05v-3H40v3Zm15.9-5.85-7.4-7.4 2.1-2.1
-            3.75 3.8V2h3v7.9l3.85-3.8 2.1 2.1Z"
-      />
-    </svg>
-  );
-};

+ 0 - 22
apps/app/src/components/Icons/CreatePageIcon.tsx

@@ -1,22 +0,0 @@
-import React from 'react';
-
-export const CreatePageIcon = (): JSX.Element => (
-  <svg
-    xmlns="http://www.w3.org/2000/svg"
-    viewBox="0 0 27 30"
-  >
-    <path
-      d="M22.81,8.2a4.2,4.2,0,0,0,1.36-2.95,4,4,0,0,0-1.43-2.81,4.53,4.53,0,0,0-1.28-.89,3.26,3.26,0,
-      0,0-1.37-.31,4,4,0,0,0-2.91,1.29q-.42.4-14.83,14.84a.7.7,0,0,0-.26.33c-.07.26-.72,2.46-2,6.58a.73.73,0,
-      0,0,.3,1,.78.78,0,0,0,.7,0c3.3-1.08,5.45-1.76,6.47-2.06A.57.57,0,0,0,7.91,23l8.5-8.42Q22.25,8.81,22.81,8.2ZM1.93,
-      23.44c.16-.44,1.39-4.39,1.5-4.78A4.93,4.93,0,0,1,5.59,20a4.53,4.53,0,0,1,1.12,1.87Zm15-18.52a4.7,4.7,0,0,1,2.16,1.31,5.08,5.08,
-      0,0,1,.72,1,5.3,5.3,0,0,1,.37.8c.05.17.09.34.13.51Q17.19,11.65,8,20.79a6.42,6.42,0,0,0-1.29-1.92,6.67,6.67,0,0,0-2.2-1.48Zm4.64,
-      2.37a6.36,6.36,0,0,0-1.36-2.13,6.61,6.61,0,0,0-2.12-1.43s.29-.28.41-.38A3,3,0,0,1,19.17,3a2,2,0,0,1,.9-.21A1.87,1.87,0,0,1,20.9,3a2.53,2.53,0,0,
-      1,.79.56,3.81,3.81,0,0,1,.71.89,1.87,1.87,0,0,1,.25.87,2.75,2.75,0,0,1-.94,1.83Z"
-    />
-    <path d="M26.41,20.05H22.84V16.48a.72.72,0,0,0-1.43,0v3.57H17.84a.72.72,0,0,0,0,1.43h3.57v3.57a.72.72,0,0,0,
-    1.43.17V21.48h3.57a.72.72,0,1,0,.17-1.43A.48.48,0,0,0,26.41,20.05Z"
-    />
-    <rect fillOpacity="0" width="27" height="27" />
-  </svg>
-);

+ 0 - 19
apps/app/src/components/Icons/ExpandIcon.tsx

@@ -1,19 +0,0 @@
-import React from 'react';
-
-export const ExpandIcon = (): JSX.Element => {
-  return (
-    <svg
-      xmlns="http://www.w3.org/2000/svg"
-      width="18"
-      height="18"
-      viewBox="0 0 45 45"
-    >
-      <path
-        fill="currentColor"
-        d="M8.1 44v-3h31.8v3Zm16-4.5-7.6-7.6 2.15-2.15
-            3.95 3.95V14.3l-3.95 3.95-2.15-2.15 7.6-7.6 7.6 7.6-2.15
-            2.15-3.95-3.95v19.4l3.95-3.95 2.15 2.15ZM8.1 7V4h31.8v3Z"
-      />
-    </svg>
-  );
-};

+ 3 - 21
apps/app/src/components/Icons/FolderIcon.tsx

@@ -9,28 +9,10 @@ export const FolderIcon = (props: Props): JSX.Element => {
   return (
     <>
       {!isOpen ? (
-        <svg
-          width="20"
-          height="20"
-          viewBox="0 0 24 24"
-        >
-          <path
-            fill="currentColor"
-            d="M20,18H4V8H20M20,6H12L10,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8C22,6.89 21.1,6 20,6Z"
-          />
-        </svg>
+        <span className="material-symbols-outlined">folder_open</span>
+
       ) : (
-        <svg
-          width="20"
-          height="20"
-          viewBox="0 0 24 24"
-        >
-          <path
-            fill="currentColor"
-            d="M6.1,10L4,18V8H21A2,2 0 0,0 19,6H12L10,4H4A2,2 0 0,0 2,6V18A2,2 0 0,0 4,
-            20H19C19.9,20 20.7,19.4 20.9,18.5L23.2,10H6.1M19,18H6L7.6,12H20.6L19,18Z"
-          />
-        </svg>
+        <span className="material-symbols-outlined">folder</span>
       )
       }
     </>

+ 0 - 16
apps/app/src/components/Icons/FolderPlusIcon.tsx

@@ -1,16 +0,0 @@
-import React from 'react';
-
-export const FolderPlusIcon = (): JSX.Element => (
-  <svg
-    width="18"
-    height="18"
-    viewBox="0 0 24 24"
-  >
-    <path
-      fill="currentColor"
-      d="M13 19C13 19.34 13.04 19.67 13.09 20H4C2.9 20 2 19.11 2 18V6C2 4.89 2.89 4 4 4H10L12 6H20C21.1 6 22
-      6.89 22 8V13.81C21.39 13.46 20.72 13.22 20 13.09V8H4V18H13.09C13.04 18.33 13 18.66 13 19M20 18V15H18V18H15V20H18V23H20V20H23V18H20Z"
-    />
-
-  </svg>
-);

+ 0 - 13
apps/app/src/components/Icons/KeyboardReturnEnterIcon.tsx

@@ -1,13 +0,0 @@
-import React from 'react';
-
-const KeyboardReturnEnterIcon = ():JSX.Element => (
-  <svg xmlns="http://www.w3.org/2000/svg" width="20px" viewBox="0 0 34 21">
-    <g id="ba5f4106-f870-416b-bb0c-2580c9a76268">
-      <g id="1def15e1-5198-4ca2-9457-3b509e83053f">
-        <polygon points="31 0 31 9 5 9 11.8 1.8 10 0 0 10.5 10 21 11.8 19.2 5 12 34 12 34 0 31 0" />
-      </g>
-    </g>
-  </svg>
-);
-
-export default KeyboardReturnEnterIcon;

+ 0 - 20
apps/app/src/components/Icons/MoonIcon.jsx

@@ -1,20 +0,0 @@
-import React from 'react';
-
-const MoonIcon = () => (
-  <svg
-    xmlns="http://www.w3.org/2000/svg"
-    viewBox="0 0 23 23"
-  >
-    <g transform="translate(-923.5 -688.5)">
-      <rect width="23" height="23" fill="none" transform="translate(923.5 688.5)" />
-      <path d="M934.893,710.532a10.646,10.646,0,0,1-10.378-8.416.7.7,0,0,1,1.138-.686,
-       7.621,7.621,0,0,0,10.721-10.744.7.7,0,0,1,.683-1.14,10.6,10.6,0,0,1-2.164,
-        20.986Zm-8.417-6.9A9.2,9.2,0,1,0,938.583,691.5a9.028,9.028,0,0,1-12.107,12.133Z"
-      />
-    </g>
-  </svg>
-
-);
-
-
-export default MoonIcon;

+ 0 - 16
apps/app/src/components/Icons/PagePreviewIcon.jsx

@@ -1,16 +0,0 @@
-import React from 'react';
-
-const PagePreviewIcon = () => (
-  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23 23">
-    <defs></defs>
-    <rect width="23" height="23" fillOpacity="0" />
-    <path d="M10.94,20.33H3.4V1.38H8.82V8.82h7.44v1.35a6.16,6.16,0,0,1,1.35.47V6.79L10.85,0H3.4a1.3,1.3,0,0,0-1,.39,1.3,1.3,0,0,0-.39,1v19A1.33,
-  1.33,0,0,0,3.4,21.68h9.84A5.94,5.94,0,0,1,10.94,20.33ZM10.17,1.38h.13l6,6v.11H10.17Z"
-    />
-    <path d="M21.87,22.14,18.75,19a4.74,4.74,0,0,0,1.1-3,4.89,4.89,0,1,0-1.8,3.73l3.11,3.11a.5.5,0,0,0,.35.15.51.51,0,0,0,.36-.15A.5.5,
-  0,0,0,21.87,22.14ZM15,19.57A3.57,3.57,0,1,1,18.59,16,3.58,3.58,0,0,1,15,19.57Z"
-    />
-  </svg>
-);
-
-export default PagePreviewIcon;

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

@@ -1,15 +0,0 @@
-import React from 'react';
-
-export const ReturnTopIcon = (): JSX.Element => (
-  <svg
-    xmlns="http://www.w3.org/2000/svg"
-    viewBox="0 0 23 23"
-  >
-    <path d="M.41,18.71a.82.82,0,0,0,0,.26.71.71,0,0,0,0,.29.5.5,0,0,0,.16.22.66.66,0,0,0,.51.21.67.67,0,0,0,
-    .51-.21l9.57-9.56,9.43,9.43a.71.71,0,0,0,.51.21.68.68,0,0,0,.51-.21.72.72,
-    0,0,0,0-1l-9.94-10a.78.78,0,0,0-.51-.19.76.76,0,0,0-.5.19L.58,18.46A.85.85,0,0,0,.41,18.71Z"
-    />
-    <path d="M22.35,4.61H.65a.65.65,0,0,1,0-1.3h21.7a.65.65,0,1,1,0,1.3Z" />
-    <rect fillOpacity="0" width="23" height="23" />
-  </svg>
-);

+ 0 - 28
apps/app/src/components/Icons/SunIcon.jsx

@@ -1,28 +0,0 @@
-import React from 'react';
-
-const SunIcon = () => (
-  <svg
-    xmlns="http://www.w3.org/2000/svg"
-    viewBox="0 0 23 23"
-  >
-    <g transform="translate(-888.497 -688.492)">
-      <rect width="23" height="23" transform="translate(888.503 688.509)" fillOpacity="0" />
-      <path d="M900,695.489a4.5,4.5,0,1,1-4.5,4.5,4.5,4.5,0,0,1,4.5-4.5m0-1.408a5.9,5.9,0,1,0,5.9,5.9,5.91,5.91,0,0,0-5.9-5.9Z" />
-      <path d="M893.968,694.573a.6.6,0,0,1-.426-.176l-1.681-1.681a.6.6,0,0,1,.853-.852l1.681,1.68a.6.6,0,0,1-.427,1.029Z" />
-      <path d="M907.707,708.295a.6.6,0,0,1-.427-.177l-1.681-1.68a.6.6,0,0,1,.854-.853l1.68,1.681a.6.6,0,0,1-.426,1.029Z" />
-
-      <path d="M899.991,692.074a.6.6,0,0,1-.6-.6v-2.377a.6.6,0,0,1,1.206,0v2.377A.6.6,0,0,1,899.991,692.074Z" />
-      <path d="M900,711.491a.6.6,0,0,1-.6-.6v-2.377a.6.6,0,1,1,1.206,0v2.377A.6.6,0,0,1,900,711.491Z" />
-
-      <path d="M906.017,694.564a.6.6,0,0,1-.426-1.029l1.68-1.68a.6.6,0,0,1,.853.854l-1.68,1.68A.6.6,0,0,1,906.017,694.564Z" />
-      <path d="M892.3,708.3a.6.6,0,0,1-.426-1.029l1.68-1.681a.6.6,0,1,1,.853.852l-1.68,1.681A.6.6,0,0,1,892.3,708.3Z" />
-
-      <path d="M910.894,700.587h-2.377a.6.6,0,1,1,0-1.2h2.377a.6.6,0,1,1,0,1.2Z" />
-      <path d="M891.477,700.6H889.1a.6.6,0,1,1,0-1.2h2.377a.6.6,0,1,1,0,1.2Z" />
-    </g>
-  </svg>
-
-);
-
-
-export default SunIcon;

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

+ 2 - 2
apps/app/src/components/Me/ApiSettings.tsx

@@ -30,10 +30,10 @@ const ApiSettings = React.memo((): JSX.Element => {
   return (
     <>
 
-      <h2 className="border-bottom my-4">{ t('API Token Settings') }</h2>
+      <h2 className="border-bottom pb-2 my-4 fs-4">{ t('API Token Settings') }</h2>
 
       <div className="row mb-3">
-        <label htmlFor="apiToken" className="col-md-3 text-md-end form-label">{t('Current API Token')}</label>
+        <label htmlFor="apiToken" className="col-md-3 text-md-end col-form-label">{t('Current API Token')}</label>
         <div className="col-md-6">
           {personalSettingsData?.apiToken != null
             ? (

+ 1 - 1
apps/app/src/components/Me/AssociateModal.tsx

@@ -66,7 +66,7 @@ const AssociateModal = (props: Props): JSX.Element => {
               className={activeTab === 1 ? 'active' : ''}
               onClick={() => setActiveTab(1)}
             >
-              <span className="material-symbols-outlined">network_node</span> LDAP
+              <span className="material-symbols-outlined fs-5">network_node</span> LDAP
             </NavLink>
             <NavLink
               className={activeTab === 2 ? 'active' : ''}

+ 2 - 2
apps/app/src/components/Me/ColorModeSettings.tsx

@@ -37,7 +37,7 @@ export const ColorModeSettings = (): JSX.Element => {
 
   return (
     <div>
-      <h2 className="border-bottom mb-4">{t('color_mode_settings.settings')}</h2>
+      <h2 className="border-bottom pb-2 mb-4 fs-4">{t('color_mode_settings.settings')}</h2>
 
       <div className="offset-md-3">
 
@@ -60,7 +60,7 @@ export const ColorModeSettings = (): JSX.Element => {
 
         </div>
 
-        <div className="mt-3 text-muted">
+        <div className="mt-3 text-muted small">
           {/* eslint-disable-next-line react/no-danger */}
           <span dangerouslySetInnerHTML={{ __html: t('color_mode_settings.description') }} />
         </div>

+ 10 - 10
apps/app/src/components/Me/ExternalAccountLinkedMe.jsx

@@ -57,18 +57,18 @@ class ExternalAccountLinkedMe extends React.Component {
 
     return (
       <Fragment>
-        <h2 className="border-bottom my-4">
-          <button
-            type="button"
-            data-testid="grw-external-account-add-button"
-            className="btn btn-outline-secondary btn-sm pull-right"
-            onClick={this.openAssociateModal}
-          >
-            <span className="material-symbols-outlined" aria-hidden="true">add_circle</span>
-            Add
-          </button>
+        <h2 className="border-bottom mt-4 pb-2 fs-4">
           { t('admin:user_management.external_accounts') }
         </h2>
+        <button
+          type="button"
+          data-testid="grw-external-account-add-button"
+          className="btn btn-outline-secondary btn-sm pull-right mb-2"
+          onClick={this.openAssociateModal}
+        >
+          <span className="material-symbols-outlined" aria-hidden="true">add_circle</span>
+          Add
+        </button>
 
         <table className="table table-bordered table-user-list">
           <thead>

+ 3 - 4
apps/app/src/components/Me/InAppNotificationSettings.tsx

@@ -1,6 +1,5 @@
-import React, {
-  FC, useState, useEffect, useCallback,
-} from 'react';
+import type { FC } from 'react';
+import React, { useState, useEffect, useCallback } from 'react';
 
 import pullAllBy from 'lodash/pullAllBy';
 import { useTranslation } from 'next-i18next';
@@ -67,7 +66,7 @@ const InAppNotificationSettings: FC = () => {
 
   return (
     <>
-      <h2 className="border-bottom my-4">{t('in_app_notification_settings.subscribe_settings')}</h2>
+      <h2 className="border-bottom pb-2 my-4 fs-4">{t('in_app_notification_settings.subscribe_settings')}</h2>
 
       <div className="row">
         <div className="offset-md-3 col-md-6 text-start">

+ 6 - 6
apps/app/src/components/Me/PasswordSettings.jsx

@@ -87,12 +87,12 @@ class PasswordSettings extends React.Component {
         ) }
 
         {(this.state.isPasswordSet)
-          ? <h2 className="border-bottom my-4">{t('personal_settings.update_password')}</h2>
-          : <h2 className="border-bottom my-4">{t('personal_settings.set_new_password')}</h2>}
+          ? <h2 className="border-bottom mt-4 mb-5 pb-2 fs-4">{t('personal_settings.update_password')}</h2>
+          : <h2 className="border-bottom mt-4 mb-5 pb-2 fs-4">{t('personal_settings.set_new_password')}</h2>}
         {(this.state.isPasswordSet)
         && (
           <div className="row mb-3">
-            <label htmlFor="oldPassword" className="col-md-3 text-md-end form-label">{ t('personal_settings.current_password') }</label>
+            <label htmlFor="oldPassword" className="col-md-3 text-md-end col-form-label">{ t('personal_settings.current_password') }</label>
             <div className="col-md-5">
               <input
                 className="form-control"
@@ -105,7 +105,7 @@ class PasswordSettings extends React.Component {
           </div>
         )}
         <div className="row mb-3">
-          <label htmlFor="newPassword" className="col-md-3 text-md-end form-label">{t('personal_settings.new_password') }</label>
+          <label htmlFor="newPassword" className="col-md-3 text-md-end col-form-label">{t('personal_settings.new_password') }</label>
           <div className="col-md-5">
             {/* to prevent autocomplete username into userForm[email] in BasicInfoSettings component */}
             {/* https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion */}
@@ -120,7 +120,7 @@ class PasswordSettings extends React.Component {
           </div>
         </div>
         <div className={`row mb-3 ${isIncorrectConfirmPassword && 'has-error'}`}>
-          <label htmlFor="newPasswordConfirm" className="col-md-3 text-md-end form-label">{t('personal_settings.new_password_confirm') }</label>
+          <label htmlFor="newPasswordConfirm" className="col-md-3 text-md-end col-form-label">{t('personal_settings.new_password_confirm') }</label>
           <div className="col-md-5">
             <input
               className="form-control"
@@ -135,7 +135,7 @@ class PasswordSettings extends React.Component {
         </div>
 
         <div className="row my-3">
-          <div className="offset-5">
+          <div className="text-center">
             <button
               data-testid="grw-password-settings-update-button"
               type="button"

+ 2 - 2
apps/app/src/components/Me/ProfileImageSettings.tsx

@@ -115,7 +115,7 @@ const ProfileImageSettings = (): JSX.Element => {
           <img src={generateGravatarSrc(currentUser.email)} className="rounded-pill" width="64" data-vrt-blackout-profile />
         </div>
 
-        <div className="col-md-7 mt-5">
+        <div className="col-md-7 mt-5 mt-md-0">
           <h5>
             <div className="form-check radio-primary">
               <input
@@ -138,7 +138,7 @@ const ProfileImageSettings = (): JSX.Element => {
             </label>
             <div className="col-md-6 col-lg-8">
               <p className="mb-0"><img src={uploadedPictureSrc ?? DEFAULT_IMAGE} className="picture picture-lg rounded-circle" id="settingUserPicture" /></p>
-              {uploadedPictureSrc && <button type="button" className="btn btn-danger" onClick={deleteImageHandler}>{ t('Delete Image') }</button>}
+              {uploadedPictureSrc && <button type="button" className="btn btn-danger mt-2" onClick={deleteImageHandler}>{ t('Delete Image') }</button>}
             </div>
           </div>
           <div className="row align-items-center mt-3 mt-md-5">

+ 9 - 9
apps/app/src/components/Me/QuestionnaireSettings.tsx

@@ -41,7 +41,7 @@ export const QuestionnaireSettings = (): JSX.Element => {
 
   return (
     <>
-      <h2 className="border-bottom mb-4">{t('questionnaire.settings')}</h2>
+      <h2 className="border-bottom pb-2 mb-4 fs-4">{t('questionnaire.settings')}</h2>
 
       {isLoadingCurrentUser && (
         <div className="text-muted text-center mb-5">
@@ -49,9 +49,9 @@ export const QuestionnaireSettings = (): JSX.Element => {
         </div>
       )}
 
-      <div className="row">
-        <div className="offset-md-3 col-md-6 text-start">
-          {!isLoadingCurrentUser && (
+      <div className="container">
+        {!isLoadingCurrentUser && (
+          <div className="offset-md-3 col-md-6 text-start row">
             <div className="form-check form-switch">
               <span id="grw-questionnaire-settings-toggle-wrapper">
                 <input
@@ -66,17 +66,17 @@ export const QuestionnaireSettings = (): JSX.Element => {
                   {t('questionnaire.enable_questionnaire')}
                 </label>
               </span>
-              <p className="form-text text-muted small">
-                {t('questionnaire.personal_settings_explanation')}
-              </p>
               {!growiIsQuestionnaireEnabled && (
                 <UncontrolledTooltip placement="bottom" target="grw-questionnaire-settings-toggle-wrapper">
                   {t('questionnaire.disabled_by_admin')}
                 </UncontrolledTooltip>
               ) }
             </div>
-          )}
-        </div>
+            <p className="form-text text-muted small">
+              {t('questionnaire.personal_settings_explanation')}
+            </p>
+          </div>
+        )}
       </div>
 
       <div className="row my-3">

+ 2 - 2
apps/app/src/components/Me/UISettings.tsx

@@ -78,16 +78,16 @@ export const UISettings = (): JSX.Element => {
             <label className="form-label form-check-label" htmlFor="swSidebarMode">
               {t('ui_settings.side_bar_mode.side_bar_mode_setting')}
             </label>
-            <p className="form-text text-muted small">{t('ui_settings.side_bar_mode.description')}</p>
           </div>
         </div>
+        <p className="form-text text-muted small">{t('ui_settings.side_bar_mode.description')}</p>
       </>
     );
   };
 
   return (
     <>
-      <h2 className="border-bottom mb-4">{t('ui_settings.ui_settings')}</h2>
+      <h2 className="border-bottom pb- mb-4 fs-4">{t('ui_settings.ui_settings')}</h2>
 
       <div className="row justify-content-center">
         <div className="col-md-6">

+ 4 - 0
apps/app/src/components/Navbar/GrowiContextualSubNavigation.module.scss

@@ -9,5 +9,9 @@
   .grw-contextual-sub-navigation {
     position: fixed;
     right: 0;
+
+    // unset colors
+    background-color: unset;
+    backdrop-filter: unset;
   }
 }

+ 3 - 1
apps/app/src/components/Navbar/PageEditorModeManager.tsx

@@ -1,7 +1,9 @@
 import React, { type ReactNode, useCallback } from 'react';
 
+import { Origin } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 
+
 import { useCreatePageAndTransit } from '~/client/services/create-page';
 import { toastError } from '~/client/util/toastr';
 import { useIsNotFound } from '~/stores/page';
@@ -74,7 +76,7 @@ export const PageEditorModeManager = (props: Props): JSX.Element => {
 
     try {
       await createAndTransit(
-        { path, wip: shouldCreateWipPage(path) },
+        { path, wip: shouldCreateWipPage(path), origin: Origin.View },
         { shouldCheckPageExists: true },
       );
     }

+ 3 - 1
apps/app/src/components/PageComment/CommentEditor.tsx

@@ -20,7 +20,7 @@ import {
   useCurrentUser, useIsSlackConfigured, useAcceptedUploadFileType,
 } from '~/stores/context';
 import {
-  useSWRxSlackChannels, useIsSlackEnabled, useIsEnabledUnsavedWarning,
+  useSWRxSlackChannels, useIsSlackEnabled, useIsEnabledUnsavedWarning, useEditorSettings,
 } from '~/stores/editor';
 import { useCurrentPagePath } from '~/stores/page';
 import { useNextThemes } from '~/stores/use-next-themes';
@@ -79,6 +79,7 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
   const { data: acceptedUploadFileType } = useAcceptedUploadFileType();
   const { data: slackChannelsData } = useSWRxSlackChannels(currentPagePath);
   const { data: isSlackConfigured } = useIsSlackConfigured();
+  const { data: editorSettings } = useEditorSettings();
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
   const {
     increment: incrementEditingCommentsNum,
@@ -336,6 +337,7 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
                 onChange={onChangeHandler}
                 onSave={postCommentHandler}
                 onUpload={uploadHandler}
+                editorSettings={editorSettings}
               />
               {/* <Editor
                 ref={editorRef}

+ 1 - 1
apps/app/src/components/PageControls/_button-styles.scss

@@ -2,7 +2,7 @@
 
 %btn-basis {
   --bs-btn-padding-x: 6px;
-  --bs-btn-padding-y: 8px;
+  --bs-btn-padding-y: 6px;
   --bs-btn-line-height: 1em;
   --bs-btn-border-width: 0;
   --bs-btn-box-shadow: none;

+ 7 - 2
apps/app/src/components/PageCreateModal.tsx

@@ -2,6 +2,7 @@ import React, {
   useEffect, useState, useMemo, useCallback,
 } from 'react';
 
+import { Origin } from '@growi/core';
 import { pagePathUtils, pathUtils } from '@growi/core/dist/utils';
 import { format } from 'date-fns';
 import { useTranslation } from 'next-i18next';
@@ -94,7 +95,7 @@ const PageCreateModal: React.FC = () => {
   const createTodayPage = useCallback(async() => {
     const joinedPath = [todaysParentPath, todayInput].join('/');
     return createAndTransit(
-      { path: joinedPath, wip: true },
+      { path: joinedPath, wip: true, origin: Origin.View },
       { shouldCheckPageExists: true, onTerminated: closeCreateModal },
     );
   }, [closeCreateModal, createAndTransit, todayInput, todaysParentPath]);
@@ -104,7 +105,11 @@ const PageCreateModal: React.FC = () => {
    */
   const createInputPage = useCallback(async() => {
     return createAndTransit(
-      { path: pageNameInput, optionalParentPath: '/', wip: true },
+      {
+        path: pageNameInput,
+        wip: true,
+        origin: Origin.View,
+      },
       { shouldCheckPageExists: true, onTerminated: closeCreateModal },
     );
   }, [closeCreateModal, createAndTransit, pageNameInput]);

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

@@ -105,7 +105,7 @@ export const Cheatsheet = (): JSX.Element => {
 
         <hr />
         <a href="/Sandbox" className="btn btn-info" target="_blank">
-          <i className="icon-share-alt" /> {t('sandbox.open_sandbox')}
+          <span className="growi-custom-icons">external_link</span> {t('sandbox.open_sandbox')}
         </a>
       </div>
     </div>

+ 0 - 0
apps/app/src/components/PageHeader/user-list-popover.module.scss → apps/app/src/components/PageEditor/EditorNavbar/EditingUserList.module.scss


+ 57 - 0
apps/app/src/components/PageEditor/EditorNavbar/EditingUserList.tsx

@@ -0,0 +1,57 @@
+import { type FC, useState } from 'react';
+
+import type { IUserHasId } from '@growi/core';
+import { UserPicture } from '@growi/ui/dist/components';
+import { Popover, PopoverBody } from 'reactstrap';
+
+import UserPictureList from '../../Common/UserPictureList';
+
+import styles from './EditingUserList.module.scss';
+
+const userListPopoverClass = styles['user-list-popover'] ?? '';
+
+type Props = {
+  userList: IUserHasId[]
+}
+
+export const EditingUserList: FC<Props> = ({ userList }) => {
+  const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+
+  const togglePopover = () => setIsPopoverOpen(!isPopoverOpen);
+
+  const firstFourUsers = userList.slice(0, 4);
+  const remainingUsers = userList.slice(4);
+
+  if (userList.length === 0) {
+    return <></>;
+  }
+
+  return (
+    <div className="d-flex flex-column justify-content-end">
+      <div className="d-flex justify-content-end">
+        {firstFourUsers.map(user => (
+          <div className="ms-1">
+            <UserPicture
+              user={user}
+              noLink
+              additionalClassName="border border-info"
+            />
+          </div>
+        ))}
+
+        {remainingUsers.length > 0 && (
+          <div className="ms-1">
+            <button type="button" id="btn-editing-user" className="btn border-0 bg-info-subtle rounded-pill p-0">
+              <span className="fw-bold text-info p-1">+{remainingUsers.length}</span>
+            </button>
+            <Popover placement="bottom" isOpen={isPopoverOpen} target="btn-editing-user" toggle={togglePopover} trigger="legacy">
+              <PopoverBody className={userListPopoverClass}>
+                <UserPictureList users={remainingUsers} />
+              </PopoverBody>
+            </Popover>
+          </div>
+        )}
+      </div>
+    </div>
+  );
+};

+ 3 - 0
apps/app/src/components/PageEditor/EditorNavbar/EditorNavbar.module.scss

@@ -0,0 +1,3 @@
+.editor-navbar :global {
+  min-height: 72px;
+}

+ 22 - 0
apps/app/src/components/PageEditor/EditorNavbar/EditorNavbar.tsx

@@ -0,0 +1,22 @@
+
+import { PageHeader } from '~/components/PageHeader';
+import { useEditingUsers } from '~/stores/use-editing-users';
+
+import { EditingUserList } from './EditingUserList';
+
+import styles from './EditorNavbar.module.scss';
+
+const moduleClass = styles['editor-navbar'] ?? '';
+
+export const EditorNavbar = (): JSX.Element => {
+  const { data: editingUsers } = useEditingUsers();
+
+  return (
+    <div className={`${moduleClass} d-flex justify-content-between px-4 py-1`}>
+      <PageHeader />
+      <EditingUserList
+        userList={editingUsers?.userList ?? []}
+      />
+    </div>
+  );
+};

+ 1 - 0
apps/app/src/components/PageEditor/EditorNavbar/index.ts

@@ -0,0 +1 @@
+export * from './EditorNavbar';

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

@@ -20,7 +20,6 @@ import { useCurrentPagePath } from '~/stores/page';
 import { usePreviewOptions } from '~/stores/renderer';
 import loggerFactory from '~/utils/logger';
 
-import PagePreviewIcon from '../Icons/PagePreviewIcon';
 import SearchTypeahead from '../SearchTypeahead';
 
 import Preview from './Preview';
@@ -256,7 +255,7 @@ export const LinkEditModal = (): JSX.Element => {
               />
               <div className="d-none d-sm-block">
                 <button type="button" id="preview-btn" className={`btn btn-info btn-page-preview ${styles['btn-page-preview']}`}>
-                  <PagePreviewIcon />
+                  <span className="material-symbols-outlined">find_in_page</span>
                 </button>
                 <Popover trigger="focus" placement="right" isOpen={isPreviewOpen} target="preview-btn" toggle={toggleIsPreviewOpen}>
                   <PopoverBody>

+ 19 - 15
apps/app/src/components/PageEditor/OptionsSelector.tsx

@@ -2,8 +2,8 @@ import React, {
   memo, useCallback, useMemo, useState,
 } from 'react';
 
-import type {
-  EditorTheme, KeyMapMode,
+import {
+  type EditorTheme, type KeyMapMode, DEFAULT_KEYMAP, DEFAULT_THEME,
 } from '@growi/editor';
 import { useTranslation } from 'next-i18next';
 import Image from 'next/image';
@@ -14,11 +14,6 @@ import {
 import { useIsIndentSizeForced } from '~/stores/context';
 import { useEditorSettings, useCurrentIndentSize } from '~/stores/editor';
 
-import {
-  DEFAULT_THEME, DEFAULT_KEYMAP,
-} from '../../interfaces/editor-settings';
-
-
 type RadioListItemProps = {
   onClick: () => void,
   icon?: React.ReactNode,
@@ -91,6 +86,7 @@ const EDITORTHEME_LABEL_MAP: EditorThemeToLabel = {
 
 const ThemeSelector = memo(({ onClickBefore }: {onClickBefore: () => void}): JSX.Element => {
 
+  const { t } = useTranslation();
   const { data: editorSettings, update } = useEditorSettings();
   const selectedTheme = editorSettings?.theme ?? DEFAULT_THEME;
 
@@ -106,7 +102,7 @@ const ThemeSelector = memo(({ onClickBefore }: {onClickBefore: () => void}): JSX
   ), [update, selectedTheme]);
 
   return (
-    <Selector header="Theme" onClickBefore={onClickBefore} items={listItems} />
+    <Selector header={t('page_edit.theme')} onClickBefore={onClickBefore} items={listItems} />
   );
 });
 ThemeSelector.displayName = 'ThemeSelector';
@@ -125,6 +121,7 @@ const KEYMAP_LABEL_MAP: KeyMapModeToLabel = {
 
 const KeymapSelector = memo(({ onClickBefore }: {onClickBefore: () => void}): JSX.Element => {
 
+  const { t } = useTranslation();
   const { data: editorSettings, update } = useEditorSettings();
   const selectedKeymapMode = editorSettings?.keymapMode ?? DEFAULT_KEYMAP;
 
@@ -144,7 +141,7 @@ const KeymapSelector = memo(({ onClickBefore }: {onClickBefore: () => void}): JS
 
 
   return (
-    <Selector header="Keymap" onClickBefore={onClickBefore} items={listItems} />
+    <Selector header={t('page_edit.keymap')} onClickBefore={onClickBefore} items={listItems} />
   );
 });
 KeymapSelector.displayName = 'KeymapSelector';
@@ -154,6 +151,7 @@ const TYPICAL_INDENT_SIZE = [2, 4];
 
 const IndentSizeSelector = memo(({ onClickBefore }: {onClickBefore: () => void}): JSX.Element => {
 
+  const { t } = useTranslation();
   const { data: currentIndentSize, mutate: mutateCurrentIndentSize } = useCurrentIndentSize();
 
   const listItems = useMemo(() => (
@@ -167,7 +165,7 @@ const IndentSizeSelector = memo(({ onClickBefore }: {onClickBefore: () => void})
   ), [currentIndentSize, mutateCurrentIndentSize]);
 
   return (
-    <Selector header="Indent" onClickBefore={onClickBefore} items={listItems} />
+    <Selector header={t('page_edit.indent')} onClickBefore={onClickBefore} items={listItems} />
   );
 });
 IndentSizeSelector.displayName = 'IndentSizeSelector';
@@ -260,6 +258,8 @@ type OptionStatus = typeof OptionsStatus[keyof typeof OptionsStatus];
 
 export const OptionsSelector = ({ collapsed }: {collapsed?: boolean}): JSX.Element => {
 
+  const { t } = useTranslation();
+
   const [dropdownOpen, setDropdownOpen] = useState(false);
 
   const [status, setStatus] = useState<OptionStatus>(OptionsStatus.Home);
@@ -282,7 +282,7 @@ export const OptionsSelector = ({ collapsed }: {collapsed?: boolean}): JSX.Eleme
         <span className="material-symbols-outlined py-0 fs-5"> settings </span>
         {
           collapsed ? <></>
-            : <label className="ms-1 me-1">Editor Config</label>
+            : <label className="ms-1 me-1">{t('page_edit.editor_config')}</label>
         }
       </DropdownToggle>
       <DropdownMenu container="body">
@@ -290,21 +290,25 @@ export const OptionsSelector = ({ collapsed }: {collapsed?: boolean}): JSX.Eleme
           status === OptionsStatus.Home && (
             <div className="d-flex flex-column">
               <label className="text-muted ms-3">
-                Editor Config
+                {t('page_edit.editor_config')}
               </label>
               <hr className="my-1" />
-              <ChangeStateButton onClick={() => setStatus(OptionsStatus.Theme)} header="Theme" data={EDITORTHEME_LABEL_MAP[editorSettings.theme ?? ''] ?? ''} />
+              <ChangeStateButton
+                onClick={() => setStatus(OptionsStatus.Theme)}
+                header={t('page_edit.theme')}
+                data={EDITORTHEME_LABEL_MAP[editorSettings.theme ?? ''] ?? ''}
+              />
               <hr className="my-1" />
               <ChangeStateButton
                 onClick={() => setStatus(OptionsStatus.Keymap)}
-                header="Keymap"
+                header={t('page_edit.keymap')}
                 data={KEYMAP_LABEL_MAP[editorSettings.keymapMode ?? ''] ?? ''}
               />
               <hr className="my-1" />
               <ChangeStateButton
                 disabled={isIndentSizeForced}
                 onClick={() => setStatus(OptionsStatus.Indent)}
-                header="Indent"
+                header={t('page_edit.indent')}
                 data={currentIndentSize.toString() ?? ''}
               />
               <hr className="my-1" />

+ 11 - 12
apps/app/src/components/PageEditor/PageEditor.tsx

@@ -6,7 +6,7 @@ import React, {
 import type EventEmitter from 'events';
 import nodePath from 'path';
 
-import type { IPageHasId, IUserHasId } from '@growi/core';
+import { type IPageHasId, Origin } from '@growi/core';
 import { useGlobalSocket } from '@growi/core/dist/swr';
 import { pathUtils } from '@growi/core/dist/utils';
 import {
@@ -57,16 +57,15 @@ import { useEditingUsers } from '~/stores/use-editing-users';
 import { useNextThemes } from '~/stores/use-next-themes';
 import loggerFactory from '~/utils/logger';
 
-import { PageHeader } from '../PageHeader/PageHeader';
-
-// import { ConflictDiffModal } from './PageEditor/ConflictDiffModal';
-// import { ConflictDiffModal } from './ConflictDiffModal';
+import { EditorNavbar } from './EditorNavbar';
 import EditorNavbarBottom from './EditorNavbarBottom';
 import Preview from './Preview';
 import { scrollEditor, scrollPreview } from './ScrollSyncHelper';
 
 import '@growi/editor/dist/style.css';
 
+// import { ConflictDiffModal } from './PageEditor/ConflictDiffModal';
+// import { ConflictDiffModal } from './ConflictDiffModal';
 
 const logger = loggerFactory('growi:PageEditor');
 
@@ -205,9 +204,9 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
 
       const { page } = await updatePage({
         pageId,
-        revisionId: currentRevisionId,
         body: codeMirrorEditor?.getDoc() ?? '',
         grant: grantData?.grant,
+        origin: Origin.Editor,
         userRelatedGrantUserGroupIds: grantData?.userRelatedGrantedGroups?.map((group) => {
           return { item: group.id, type: group.type };
         }),
@@ -433,9 +432,9 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
 
   return (
     <div data-testid="page-editor" id="page-editor" className={`flex-expand-vert ${props.visibility ? '' : 'd-none'}`}>
-      <div className="px-4 py-2">
-        <PageHeader />
-      </div>
+
+      <EditorNavbar />
+
       <div className={`flex-expand-horiz ${props.visibility ? '' : 'd-none'}`}>
         <div className="page-editor-editor-container flex-expand-vert">
           <CodeMirrorEditorMain
@@ -448,10 +447,8 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
             user={user ?? undefined}
             pageId={pageId ?? undefined}
             initialValue={initialValue}
-            onOpenEditor={markdown => setMarkdownToPreview(markdown)}
+            editorSettings={editorSettings}
             onEditorsUpdated={onEditorsUpdated}
-            editorTheme={editorSettings?.theme}
-            editorKeymap={editorSettings?.keymapMode}
           />
         </div>
         <div
@@ -477,7 +474,9 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
         />
         */}
       </div>
+
       <EditorNavbarBottom />
+
     </div>
   );
 });

+ 0 - 53
apps/app/src/components/PageHeader/EditingUserList.tsx

@@ -1,53 +0,0 @@
-import { type FC, useState } from 'react';
-
-import type { IUserHasId } from '@growi/core';
-import { UserPicture } from '@growi/ui/dist/components';
-import { Popover, PopoverBody } from 'reactstrap';
-
-import UserPictureList from '../Common/UserPictureList';
-
-import popoverStyles from './user-list-popover.module.scss';
-
-type Props = {
-  className: string,
-  userList: IUserHasId[]
-}
-
-export const EditingUserList: FC<Props> = ({ className, userList }) => {
-  const [isPopoverOpen, setIsPopoverOpen] = useState(false);
-
-  const togglePopover = () => setIsPopoverOpen(!isPopoverOpen);
-
-  const firstFourUsers = userList.slice(0, 4);
-  const remainingUsers = userList.slice(4);
-
-  return (
-    <div className={className}>
-      {userList.length > 0 && (
-        <div className="d-flex justify-content-end">
-          {firstFourUsers.map(user => (
-            <div className="ms-1">
-              <UserPicture
-                user={user}
-                noLink
-                additionalClassName="border border-info"
-              />
-            </div>
-          ))}
-          {remainingUsers.length > 0 && (
-            <div className="ms-1">
-              <button type="button" id="btn-editing-user" className="btn border-0 bg-info-subtle rounded-pill p-0">
-                <span className="fw-bold text-info p-1">+{remainingUsers.length}</span>
-              </button>
-              <Popover placement="bottom" isOpen={isPopoverOpen} target="btn-editing-user" toggle={togglePopover} trigger="legacy">
-                <PopoverBody className={`user-list-popover ${popoverStyles['user-list-popover']}`}>
-                  <UserPictureList users={remainingUsers} />
-                </PopoverBody>
-              </Popover>
-            </div>
-          )}
-        </div>
-      )}
-    </div>
-  );
-};

+ 1 - 12
apps/app/src/components/PageHeader/PageHeader.tsx

@@ -1,11 +1,7 @@
 import type { FC } from 'react';
 
-import { DevidedPagePath } from '@growi/core/dist/models';
-
 import { useSWRxCurrentPage } from '~/stores/page';
-import { useEditingUsers } from '~/stores/use-editing-users';
 
-import { EditingUserList } from './EditingUserList';
 import { PagePathHeader } from './PagePathHeader';
 import { PageTitleHeader } from './PageTitleHeader';
 
@@ -15,28 +11,21 @@ const moduleClass = styles['page-header'] ?? '';
 
 export const PageHeader: FC = () => {
   const { data: currentPage } = useSWRxCurrentPage();
-  const { data: editingUsers } = useEditingUsers();
 
   if (currentPage == null) {
     return <></>;
   }
 
-  const dPagePath = new DevidedPagePath(currentPage.path, true);
-
   return (
     <div className={moduleClass}>
       <PagePathHeader
         currentPage={currentPage}
       />
-      <div className="row mt-2">
+      <div className="row mt-1">
         <PageTitleHeader
           className="col"
           currentPage={currentPage}
         />
-        <EditingUserList
-          className={`${dPagePath.isRoot ? 'mt-1' : 'col mt-2'}`}
-          userList={editingUsers?.userList ?? []}
-        />
       </div>
     </div>
   );

+ 1 - 1
apps/app/src/components/PageHeader/PagePathHeader.module.scss

@@ -13,7 +13,7 @@
     .btn {
       width: 24px;
       height: 24px;
-      transform: translateY(8px);
+      transform: translateY(12px);
     }
   }
 }

+ 1 - 0
apps/app/src/components/PageHeader/index.ts

@@ -0,0 +1 @@
+export * from './PageHeader';

+ 6 - 6
apps/app/src/components/PageSideContents/PageAccessoriesControl.module.scss

@@ -18,12 +18,12 @@
   }
 }
 
-// apply larger font when smaller than lg
-@include bs.media-breakpoint-down(lg) {
-  .btn-page-accessories :global {
-    .material-symbols-outlined {
-      font-size: 2em;
-    }
+// apply font-size
+.btn-page-accessories :global {
+  --bs-btn-font-size: 14px;
+
+  @include bs.media-breakpoint-down(lg) {
+    --bs-btn-font-size: 16px;
   }
 }
 

+ 1 - 1
apps/app/src/components/PageSideContents/PageAccessoriesControl.tsx

@@ -27,7 +27,7 @@ export const PageAccessoriesControl = memo((props: Props): JSX.Element => {
   return (
     <button
       type="button"
-      className={`btn btn-sm btn-outline-neutral-secondary ${moduleClass} ${className} rounded-pill`}
+      className={`btn btn-outline-neutral-secondary ${moduleClass} ${className} rounded-pill`}
       onClick={onClick}
     >
       <span className="grw-icon d-flex">{icon}</span>

+ 9 - 5
apps/app/src/components/PageSideContents/PageSideContents.tsx

@@ -1,4 +1,4 @@
-import React, { Suspense, useCallback } from 'react';
+import React, { Suspense, useCallback, useRef } from 'react';
 
 import type { IPagePopulatedToShowRevision } from '@growi/core';
 import { getIdForRef, type IPageInfoForOperation } from '@growi/core';
@@ -82,6 +82,8 @@ export const PageSideContents = (props: PageSideContentsProps): JSX.Element => {
 
   const { page, isSharedUser } = props;
 
+  const tagsRef = useRef<HTMLDivElement>(null);
+
   const { data: pageInfo } = useSWRxPageInfo(page._id);
 
   const pagePath = page.path;
@@ -93,9 +95,11 @@ export const PageSideContents = (props: PageSideContentsProps): JSX.Element => {
     <>
       {/* Tags */}
       { page.revision != null && (
-        <Suspense fallback={<PageTagsSkeleton />}>
-          <Tags pageId={page._id} revisionId={page.revision._id} />
-        </Suspense>
+        <div ref={tagsRef}>
+          <Suspense fallback={<PageTagsSkeleton />}>
+            <Tags pageId={page._id} revisionId={page.revision._id} />
+          </Suspense>
+        </div>
       ) }
 
       <div className={`${styles['grw-page-accessories-controls']} d-flex flex-column gap-2`}>
@@ -127,7 +131,7 @@ export const PageSideContents = (props: PageSideContentsProps): JSX.Element => {
       </div>
 
       <div className="d-none d-xl-block">
-        <TableOfContents />
+        <TableOfContents tagsElementHeight={tagsRef.current?.clientHeight} />
         {isUsersHomepagePath && <ContentLinkButtons author={page?.creator} />}
       </div>
     </>

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

@@ -45,7 +45,7 @@ export const PageTags:FC<Props> = (props: Props) => {
           <NotAvailableForReadOnlyUser>
             <button
               type="button"
-              className={`btn btn-sm btn-outline-secondary rounded-pill ${styles['grw-tag-icon-button']}`}
+              className={`btn btn-edit-tags btn-outline-neutral-secondary rounded-pill ${styles['grw-tag-icon-button']}`}
               onClick={onClickEditTagsButton}
             >
               <span className="material-symbols-outlined">local_offer</span>
@@ -58,7 +58,7 @@ export const PageTags:FC<Props> = (props: Props) => {
           <button
             id="edit-tags-btn-wrapper-for-tooltip"
             type="button"
-            className="btn btn-link text-secondary p-0 border-0"
+            className="btn btn-link btn-edit-tags text-secondary p-0 border-0"
             onMouseEnter={onMouseEnterHandler}
             onMouseLeave={onMouseLeaveHandler}
             onClick={onClickEditTagsButton}

+ 1 - 1
apps/app/src/components/PageTags/RenderTagLabels.tsx

@@ -20,7 +20,7 @@ const RenderTagLabels = React.memo((props: RenderTagLabelsProps) => {
         <a
           key={tag}
           type="button"
-          className="grw-tag badge me-1 mb-1 text-truncate"
+          className="grw-tag badge me-1 mb-1 text-truncate mw-100"
           onClick={() => pushState(`tag:${tag}`)}
         >
           {tag}

+ 1 - 1
apps/app/src/components/PageTags/TagEditModal.tsx

@@ -52,7 +52,7 @@ const TagEditModalSubstance: React.FC<TagEditModalSubstanceProps> = (props: TagE
 
   return (
     <Modal isOpen={isOpen} toggle={closeTagEditModal} id="edit-tag-modal" autoFocus={false}>
-      <ModalHeader tag="h4" toggle={closeTagEditModal} className="bg-primary text-light">
+      <ModalHeader tag="h4" toggle={closeTagEditModal}>
         {t('tag_edit_modal.edit_tags')}
       </ModalHeader>
       <ModalBody>

+ 9 - 8
apps/app/src/components/PageTags/TagLabels.module.scss

@@ -12,20 +12,21 @@ $grw-tag-label-font-size: 12px;
   .grw-tag-simple-bar {
     width: 15.5rem;
     max-height: 5rem;
-    .grw-tag{
-      max-width: 15rem;
-    }
   }
 
-  // apply larger font when smaller than lg
-  @include bs.media-breakpoint-down(lg) {
-    .material-symbols-outlined {
-      font-size: 2em;
+}
+
+// apply font-size
+.grw-tag-labels :global {
+  .btn-edit-tags {
+    --bs-btn-font-size: 14px;
+
+    @include bs.media-breakpoint-down(lg) {
+      --bs-btn-font-size: 16px;
     }
   }
 }
 
-
 .grw-tag-labels-skeleton :global {
   width: 137px;
   height: calc(#{$grw-tag-label-font-size} + #{bs.$badge-padding-y} * 2);

+ 23 - 0
apps/app/src/components/PageTags/TagsInput.module.scss

@@ -0,0 +1,23 @@
+.tags-input :global {
+  .rbt-token {
+    .rbt-token-label {
+      // override to text-truncate
+      overflow: hidden;
+      font-size: 1rem; // adjust font-size
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+  }
+}
+
+// == Colors
+.tags-input :global {
+  .rbt-token {
+    // override to .badge color
+    color: var(--bs-badge-color);
+  }
+
+  .rbt-token-active {
+    border-color: var(--grw-primary-400) !important;
+  }
+}

+ 12 - 2
apps/app/src/components/PageTags/TagsInput.tsx

@@ -3,10 +3,12 @@ import React, { useRef, useState, useCallback } from 'react';
 
 import { useTranslation } from 'next-i18next';
 import type { TypeaheadRef } from 'react-bootstrap-typeahead';
-import { AsyncTypeahead } from 'react-bootstrap-typeahead';
+import { AsyncTypeahead, Token } from 'react-bootstrap-typeahead';
 
 import { useSWRxTagsSearch } from '~/stores/tag';
 
+import styles from './TagsInput.module.scss';
+
 type Props = {
   tags: string[],
   autoFocus: boolean,
@@ -50,7 +52,7 @@ export const TagsInput: FC<Props> = (props: Props) => {
   }, []);
 
   return (
-    <div className="tag-typeahead">
+    <div className={`${styles['tags-input']}`}>
       <AsyncTypeahead
         id="tag-typeahead-asynctypeahead"
         ref={tagsInputRef}
@@ -64,6 +66,14 @@ export const TagsInput: FC<Props> = (props: Props) => {
         options={resultTags} // Search result (Some tag names)
         placeholder={t('tag_edit_modal.tags_input.tag_name')}
         autoFocus={autoFocus}
+        // option is tag name
+        renderToken={(option: string, { onRemove }, idx) => {
+          return (
+            <Token key={idx} className="grw-tag badge mw-100 d-inline-flex p-0" option={option} onRemove={onRemove}>
+              {option}
+            </Token>
+          );
+        }}
       />
     </div>
   );

+ 3 - 2
apps/app/src/components/ReactMarkdownComponents/NextLink.tsx

@@ -1,5 +1,6 @@
 import { pagePathUtils } from '@growi/core/dist/utils';
-import Link, { LinkProps } from 'next/link';
+import type { LinkProps } from 'next/link';
+import Link from 'next/link';
 
 import { useSiteUrl } from '~/stores/context';
 import loggerFactory from '~/utils/logger';
@@ -61,7 +62,7 @@ export const NextLink = (props: Props): JSX.Element => {
   if (isExternalLink(href, siteUrl)) {
     return (
       <a id={id} href={href} className={className} target="_blank" rel="noopener noreferrer" {...dataAttributes}>
-        {children}&nbsp;<i className="icon-share-alt small"></i>
+        {children}&nbsp;<span className="growi-custom-icons">external_link</span>
       </a>
     );
   }

+ 1 - 2
apps/app/src/components/ShortcutsModal.tsx

@@ -3,7 +3,6 @@ import React from 'react';
 import { useTranslation } from 'next-i18next';
 import { Modal, ModalHeader, ModalBody } from 'reactstrap';
 
-import KeyboardReturnEnterIcon from '~/components/Icons/KeyboardReturnEnterIcon';
 import { useShortcutsModal } from '~/stores/modal';
 
 import styles from './ShortcutsModal.module.scss';
@@ -142,7 +141,7 @@ const ShortcutsModal = (): JSX.Element => {
                   <td className="text-nowrap">
                     <span className={`key cmd-key ${additionalClassByOs}`}></span> +
                     <span className="key key-longer">
-                      <KeyboardReturnEnterIcon />
+                      <span className="material-symbols-outlined">keyboard_return</span>
                     </span>
                   </td>
                 </tr>

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

@@ -6,7 +6,6 @@ import { apiv3Post } from '~/client/util/apiv3-client';
 import { toastError } from '~/client/util/toastr';
 import { BookmarkFolderNameInput } from '~/components/Bookmarks/BookmarkFolderNameInput';
 import { BookmarkFolderTree } from '~/components/Bookmarks/BookmarkFolderTree';
-import { FolderPlusIcon } from '~/components/Icons/FolderPlusIcon';
 import { useSWRxBookmarkFolderAndChild } from '~/stores/bookmark-folder';
 import { useCurrentUser } from '~/stores/context';
 
@@ -47,7 +46,7 @@ export const BookmarkContents = (): JSX.Element => {
         >
 
           <div className="d-flex align-items-center">
-            <FolderPlusIcon />
+            <span className="material-symbols-outlined">create_new_folder</span>
             <span className="ms-2">{t('bookmark_folder.new_folder')}</span>
           </div>
         </button>

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

@@ -1,5 +1,6 @@
 import { useCallback } from 'react';
 
+import { Origin } from '@growi/core';
 import { useTranslation } from 'react-i18next';
 
 import { useCreatePageAndTransit } from '~/client/services/create-page';
@@ -10,7 +11,7 @@ export const SidebarNotFound = (): JSX.Element => {
   const { createAndTransit } = useCreatePageAndTransit();
 
   const clickCreateButtonHandler = useCallback(async() => {
-    createAndTransit({ path: '/Sidebar', wip: false });
+    createAndTransit({ path: '/Sidebar', wip: false, origin: Origin.View });
   }, [createAndTransit]);
 
   return (

+ 8 - 1
apps/app/src/components/Sidebar/PageCreateButton/hooks/use-create-new-page.ts

@@ -1,5 +1,7 @@
 import { useCallback } from 'react';
 
+import { Origin } from '@growi/core';
+
 import { useCreatePageAndTransit } from '~/client/services/create-page';
 import { useCurrentPagePath } from '~/stores/page';
 
@@ -18,7 +20,12 @@ export const useCreateNewPage: UseCreateNewPage = () => {
     if (isLoadingPagePath) return;
 
     return createAndTransit(
-      { parentPath: currentPagePath, optionalParentPath: '/', wip: true },
+      {
+        parentPath: currentPagePath,
+        optionalParentPath: '/',
+        wip: true,
+        origin: Origin.View,
+      },
     );
   }, [createAndTransit, currentPagePath, isLoadingPagePath]);
 

+ 2 - 1
apps/app/src/components/Sidebar/PageCreateButton/hooks/use-create-todays-memo.tsx

@@ -1,5 +1,6 @@
 import { useCallback } from 'react';
 
+import { Origin } from '@growi/core';
 import { userHomepagePath } from '@growi/core/dist/utils/page-path-utils';
 import { format } from 'date-fns';
 import { useTranslation } from 'react-i18next';
@@ -32,7 +33,7 @@ export const useCreateTodaysMemo: UseCreateTodaysMemo = () => {
     if (!isCreatable || todaysPath == null) return;
 
     return createAndTransit(
-      { path: todaysPath, wip: true },
+      { path: todaysPath, wip: true, origin: Origin.View },
       { shouldCheckPageExists: true },
     );
   }, [createAndTransit, isCreatable, todaysPath]);

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

+ 33 - 0
apps/app/src/components/Sidebar/SidebarNav/PersonalDropdown.module.scss

@@ -0,0 +1,33 @@
+@use '@growi/core/scss/bootstrap/init' as bs;
+
+@include bs.color-mode(light) {
+  .personal-dropdown-header :global {
+    color: var(--bs-gray-600);
+  }
+
+  .personal-dropdown-item :global {
+    --bs-link-color-rgb:var(--bs-gray-600);
+    color: var(--bs-gray-600);
+  }
+}
+
+@include bs.color-mode(dark) {
+  .personal-dropdown-header :global {
+    color: var(--bs-gray-500);
+  }
+
+  .personal-dropdown-item :global {
+    --bs-link-color-rgb:var(--bs-gray-500);
+    color: var(--bs-gray-500);
+  }
+}
+
+.personal-dropdown-menu :global {
+  --bs-dropdown-font-size: 14px;
+}
+
+.personal-dropdown-header :global {
+  .item-text-email {
+    font-size: 10.5px;
+  }
+}

+ 40 - 32
apps/app/src/components/Sidebar/SidebarNav/PersonalDropdown.tsx

@@ -15,6 +15,8 @@ import { useCurrentUser } from '~/stores/context';
 
 import { SkeletonItem } from './SkeletonItem';
 
+import styles from './PersonalDropdown.module.scss';
+
 const ProactiveQuestionnaireModal = dynamic(() => import('~/features/questionnaire/client/components/ProactiveQuestionnaireModal'), { ssr: false });
 
 export const PersonalDropdown = (): JSX.Element => {
@@ -52,58 +54,64 @@ export const PersonalDropdown = (): JSX.Element => {
         <DropdownMenu
           container="body"
           data-testid="personal-dropdown-menu"
+          className={styles['personal-dropdown-menu']}
         >
-          <DropdownItem header>
-            <div className="mt-2">
+          <DropdownItem className={styles['personal-dropdown-header']}>
+            <div className="mt-2 mb-3">
               <UserPicture user={currentUser} size="lg" noLink noTooltip />
             </div>
-            <div className="mt-3 ms-1 fs-5">{currentUser.name}</div>
-            <div className="mt-2 d-flex align-items-center">
-              <span className="material-symbols-outlined me-1">person</span>
-              {currentUser.username}
+            <div className="ms-1 fs-6">{currentUser.name}</div>
+            <div className="d-flex align-items-center my-2">
+              <small className="material-symbols-outlined me-1 pb-0 fs-6">person</small>
+              <span>{currentUser.username}</span>
             </div>
             <div className="d-flex align-items-center">
-              <span className="material-symbols-outlined me-1">mail</span>
-              <span className="grw-email-sm">{currentUser.email}</span>
+              <span className="material-symbols-outlined me-1 pb-0 fs-6">mail</span>
+              <span className="item-text-email">{currentUser.email}</span>
             </div>
           </DropdownItem>
 
-          <DropdownItem divider />
+          <DropdownItem className="my-3" divider />
 
-          <DropdownItem>
-            <Link
-              href={pagePathUtils.userHomepagePath(currentUser)}
-              data-testid="grw-personal-dropdown-menu-user-home"
-            >
-              <span className="text-muted">
-                <span className="material-symbols-outlined me-1">home</span>{t('personal_dropdown.home')}
+          <Link
+            href={pagePathUtils.userHomepagePath(currentUser)}
+            data-testid="grw-personal-dropdown-menu-user-home"
+          >
+            <DropdownItem className={`my-1 ${styles['personal-dropdown-item']}`}>
+              <span className="d-flex align-items-center">
+                <span className="item-icon material-symbols-outlined me-2 pb-0 fs-6">home</span>
+                <span className="item-text">{t('personal_dropdown.home')}</span>
               </span>
-            </Link>
-          </DropdownItem>
+            </DropdownItem>
+          </Link>
 
-          <DropdownItem>
-            <Link
-              href="/me"
-              data-testid="grw-personal-dropdown-menu-user-settings"
-            >
-              <span className="text-muted">
-                <span className="material-symbols-outlined me-1">build</span>{t('personal_dropdown.settings')}
+          <Link
+            href="/me"
+            data-testid="grw-personal-dropdown-menu-user-settings"
+          >
+            <DropdownItem className={`my-1 ${styles['personal-dropdown-item']}`}>
+              <span className="d-flex align-items-center">
+                <span className="item-icon material-symbols-outlined me-2 pb-0 fs-6">discover_tune</span>
+                <span className="item-text">{t('personal_dropdown.settings')}</span>
               </span>
-            </Link>
-          </DropdownItem>
+            </DropdownItem>
+          </Link>
 
           <DropdownItem
             data-testid="grw-proactive-questionnaire-modal-toggle-btn"
             onClick={() => setQuestionnaireModalOpen(true)}
+            className={`my-1 ${styles['personal-dropdown-item']}`}
           >
-            <span className="text-muted">
-              <span className="material-symbols-outlined me-1">edit</span>{t('personal_dropdown.feedback')}
+            <span className="d-flex align-items-center">
+              <span className="item-icon material-symbols-outlined me-2 pb-0 fs-6">edit_note</span>
+              <span className="item-text">{t('personal_dropdown.feedback')}</span>
             </span>
           </DropdownItem>
 
-          <DropdownItem onClick={logoutHandler}>
-            <span className="text-muted">
-              <span className="material-symbols-outlined me-1">logout</span>{t('Sign out')}
+          <DropdownItem onClick={logoutHandler} className={`my-1 ${styles['personal-dropdown-item']}`}>
+            <span className="d-flex align-items-center">
+              <span className="item-icon material-symbols-outlined me-2 pb-0 fs-6">logout</span>
+              <span className="item-text">{t('Sign out')}</span>
             </span>
           </DropdownItem>
         </DropdownMenu>

+ 3 - 2
apps/app/src/components/SlackNotification.tsx

@@ -1,5 +1,6 @@
 /* eslint-disable react/prop-types */
-import React, { FC } from 'react';
+import type { FC } from 'react';
+import React from 'react';
 
 import { useTranslation } from 'next-i18next';
 import { PopoverBody, PopoverHeader, UncontrolledPopover } from 'reactstrap';
@@ -56,7 +57,7 @@ export const SlackNotification: FC<SlackNotificationProps> = ({
           id={idForSlackPopover}
           type="text"
           value={slackChannels}
-          placeholder="Input channels"
+          placeholder={t('page_edit.input_channels', 'Input channels')}
           onChange={updateSlackChannelsHandler}
         />
         <UncontrolledPopover trigger="focus" placement="top" target={idForSlackPopover}>

+ 7 - 3
apps/app/src/components/TableOfContents.tsx

@@ -16,7 +16,11 @@ const { isUsersHomepage: _isUsersHomepage } = pagePathUtils;
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
 const logger = loggerFactory('growi:TableOfContents');
 
-const TableOfContents = (): JSX.Element => {
+type Props = {
+  tagsElementHeight?: number
+}
+
+const TableOfContents = ({ tagsElementHeight }: Props): JSX.Element => {
   const { data: currentPagePath } = useCurrentPagePath();
 
   const isUsersHomePage = currentPagePath != null && _isUsersHomepage(currentPagePath);
@@ -30,7 +34,7 @@ const TableOfContents = (): JSX.Element => {
 
     // rendererOptions for redo calcViewHeight()
     // see: https://github.com/weseek/growi/pull/6791
-    if (parentElem == null || containerElem == null || rendererOptions == null) {
+    if (parentElem == null || containerElem == null || rendererOptions == null || tagsElementHeight == null) {
       return 0;
     }
     const parentBottom = parentElem.getBoundingClientRect().bottom;
@@ -47,7 +51,7 @@ const TableOfContents = (): JSX.Element => {
     }
     // bottom - revisionToc top
     return bottom - (containerTop + containerPaddingTop);
-  }, [isUsersHomePage, rendererOptions]);
+  }, [isUsersHomePage, rendererOptions, tagsElementHeight]);
 
   return (
     <div id="revision-toc" className={`revision-toc ${styles['revision-toc']}`}>

+ 4 - 1
apps/app/src/components/TreeItem/NewPageInput/use-new-page-input.tsx

@@ -1,5 +1,7 @@
 import React, { useState, type FC, useCallback } from 'react';
 
+import { Origin } from '@growi/core';
+
 import { createPage } from '~/client/services/page-operation';
 import { useSWRxPageChildren, mutatePageTree } from '~/stores/page-listing';
 import { usePageTreeDescCountMap } from '~/stores/ui';
@@ -75,6 +77,7 @@ export const useNewPageInput = (): UseNewPageInput => {
         // keep grant info undefined to inherit from parent
         grant: undefined,
         grantUserGroupIds: undefined,
+        origin: Origin.View,
         wip: shouldCreateWipPage(newPagePath),
       });
 
@@ -83,7 +86,7 @@ export const useNewPageInput = (): UseNewPageInput => {
       if (!hasDescendants) {
         stateHandlers?.setIsOpen(true);
       }
-    }, [hasDescendants, mutateChildren, stateHandlers]);
+    }, [hasDescendants, stateHandlers]);
 
     const submittionFailedHandler = useCallback(() => {
       setProcessingSubmission(false);

+ 10 - 10
apps/app/src/components/UsersHomepageFooter.module.scss

@@ -4,7 +4,10 @@ $grw-sidebar-content-footer-height: 50px;
 
 .user-page-footer :global {
   .grw-user-page-list-m {
-    .list-group{
+    .growi-custom-icons {
+      font-size: 1.1em;
+    }
+    .list-group {
       .list-group-item {
         .grw-visible-on-hover {
           display: none;
@@ -15,20 +18,19 @@ $grw-sidebar-content-footer-height: 50px;
             display: block;
           }
         }
-        .grw-triangle-container{
+        .grw-triangle-container {
           svg {
             width: 12px;
             height: 12px;
           }
         }
-        svg{
+        svg {
           width: 20px;
           height: 20px;
         }
         min-height: 40px;
         border-radius: 0px;
 
-
         &.grw-bookmark-item-list {
           .picture {
             width: 16px;
@@ -40,17 +42,15 @@ $grw-sidebar-content-footer-height: 50px;
               height: 20px;
             }
           }
-          svg{
+          svg {
             width: 14px;
             height: 14px;
           }
-          .grw-foldertree-control{
+          .grw-foldertree-control {
             margin-left: 1rem;
           }
         }
       }
-
-
     }
 
     .grw-foldertree-item-container {
@@ -58,7 +58,7 @@ $grw-sidebar-content-footer-height: 50px;
         max-width: 25%;
       }
     }
-    .grw-foldertree-title-anchor{
+    .grw-foldertree-title-anchor {
       width: fit-content !important;
       margin-right: 20px;
     }
@@ -67,7 +67,7 @@ $grw-sidebar-content-footer-height: 50px;
       height: 35px;
       margin-bottom: 6px;
     }
-    .new-bookmark-folder{
+    .new-bookmark-folder {
       max-height: 30px;
       svg {
         width: 18px;

+ 5 - 15
apps/app/src/components/UsersHomepageFooter.tsx

@@ -2,19 +2,16 @@ import React, { useState } from 'react';
 
 import { useTranslation } from 'next-i18next';
 
-
 import { RecentlyCreatedIcon } from '~/components/Icons/RecentlyCreatedIcon';
 import { RecentCreated } from '~/components/RecentCreated/RecentCreated';
 import styles from '~/components/UsersHomepageFooter.module.scss';
 import { useCurrentUser } from '~/stores/context';
 
 import { BookmarkFolderTree } from './Bookmarks/BookmarkFolderTree';
-import { CompressIcon } from './Icons/CompressIcon';
-import { ExpandIcon } from './Icons/ExpandIcon';
 
 export type UsersHomepageFooterProps = {
-  creatorId: string,
-}
+  creatorId: string;
+};
 
 export const UsersHomepageFooter = (props: UsersHomepageFooterProps): JSX.Element => {
   const { t } = useTranslation();
@@ -30,15 +27,8 @@ export const UsersHomepageFooter = (props: UsersHomepageFooterProps): JSX.Elemen
           <span style={{ fontSize: '1.3em' }} className="material-symbols-outlined">bookmark</span>
           {t('footer.bookmarks')}
           <span className="ms-auto ps-2 ">
-            <button
-              type="button"
-              className={`btn btn-sm grw-expand-compress-btn ${isExpanded ? 'active' : ''}`}
-              onClick={() => setIsExpanded(!isExpanded)}
-            >
-              { isExpanded
-                ? <ExpandIcon />
-                : <CompressIcon />
-              }
+            <button type="button" className={`btn btn-sm grw-expand-compress-btn ${isExpanded ? 'active' : ''}`} onClick={() => setIsExpanded(!isExpanded)}>
+              {isExpanded ? <span className="material-symbols-outlined">expand</span> : <span className="material-symbols-outlined">compress</span>}
             </button>
           </span>
         </h2>
@@ -49,7 +39,7 @@ export const UsersHomepageFooter = (props: UsersHomepageFooterProps): JSX.Elemen
       </div>
       <div className="grw-user-page-list-m mt-5 d-edit-none">
         <h2 id="recently-created-list" className="grw-user-page-header border-bottom pb-2 mb-3">
-          <i id="recent-created-icon" className="me-1"><RecentlyCreatedIcon /></i>
+          <span className="growi-custom-icons me-1">recently_created</span>
           {t('footer.recently_created')}
         </h2>
         <div id="user-created-list" className={`page-list ${styles['page-list']}`}>

+ 18 - 3
apps/app/src/features/external-user-group/client/components/ExternalUserGroup/ExternalUserGroupManagement.tsx

@@ -1,6 +1,8 @@
 import type { FC } from 'react';
 import { useCallback, useMemo, useState } from 'react';
 
+import type { IGrantedGroup } from '@growi/core';
+import { GroupType, getIdForRef } from '@growi/core';
 import { useTranslation } from 'react-i18next';
 import { TabContent, TabPane } from 'reactstrap';
 
@@ -11,17 +13,25 @@ import { UserGroupModal } from '~/components/Admin/UserGroup/UserGroupModal';
 import { UserGroupTable } from '~/components/Admin/UserGroup/UserGroupTable';
 import CustomNav from '~/components/CustomNavigation/CustomNav';
 import type { IExternalUserGroupHasId } from '~/features/external-user-group/interfaces/external-user-group';
+import type { PageActionOnGroupDelete } from '~/interfaces/user-group';
 import { useIsAclEnabled } from '~/stores/context';
+import { useSWRxUserGroupList } from '~/stores/user-group';
 
 import { useSWRxChildExternalUserGroupList, useSWRxExternalUserGroupList, useSWRxExternalUserGroupRelationList } from '../../stores/external-user-group';
 
 import { KeycloakGroupManagement } from './KeycloakGroupManagement';
 import { LdapGroupManagement } from './LdapGroupManagement';
-import { PageActionOnGroupDelete } from '~/interfaces/user-group';
 
 export const ExternalGroupManagement: FC = () => {
   const { data: externalUserGroupList, mutate: mutateExternalUserGroups } = useSWRxExternalUserGroupList();
+  const { data: userGroupList } = useSWRxUserGroupList();
   const externalUserGroups = externalUserGroupList != null ? externalUserGroupList : [];
+  const externalUserGroupsForDeleteModal: IGrantedGroup[] = externalUserGroups.map((group) => {
+    return { item: group, type: GroupType.externalUserGroup };
+  });
+  const userGroupsForDeleteModal: IGrantedGroup[] = userGroupList != null ? userGroupList.map((group) => {
+    return { item: group, type: GroupType.userGroup };
+  }) : [];
   const externalUserGroupIds = externalUserGroups.map(group => group._id);
 
   const { data: externalUserGroupRelationList } = useSWRxExternalUserGroupRelationList(externalUserGroupIds);
@@ -93,11 +103,16 @@ export const ExternalGroupManagement: FC = () => {
     }
   }, [t, mutateExternalUserGroups, hideUpdateModal]);
 
-  const deleteExternalUserGroupById = useCallback(async(deleteGroupId: string, actionName: PageActionOnGroupDelete, transferToUserGroupId: string) => {
+  const deleteExternalUserGroupById = useCallback(async(
+      deleteGroupId: string, actionName: PageActionOnGroupDelete, transferToUserGroup: IGrantedGroup | null,
+  ) => {
+    const transferToUserGroupId = transferToUserGroup != null ? getIdForRef(transferToUserGroup.item) : null;
+    const transferToUserGroupType = transferToUserGroup != null ? transferToUserGroup.type : null;
     try {
       await apiv3Delete(`/external-user-groups/${deleteGroupId}`, {
         actionName,
         transferToUserGroupId,
+        transferToUserGroupType,
       });
 
       // sync
@@ -154,7 +169,7 @@ export const ExternalGroupManagement: FC = () => {
       />
 
       <UserGroupDeleteModal
-        userGroups={externalUserGroups}
+        userGroups={userGroupsForDeleteModal.concat(externalUserGroupsForDeleteModal)}
         deleteUserGroup={selectedExternalUserGroup}
         onDelete={deleteExternalUserGroupById}
         isShow={isDeleteModalShown}

+ 8 - 6
apps/app/src/features/external-user-group/server/routes/apiv3/external-user-group.ts

@@ -150,17 +150,19 @@ module.exports = (crowi: Crowi): Router => {
   router.delete('/:id', loginRequiredStrictly, adminRequired, validators.delete, apiV3FormValidator, addActivity,
     async(req: AuthorizedRequest, res: ApiV3Response) => {
       const { id: deleteGroupId } = req.params;
-      const { transferToUserGroupId } = req.query;
+      const { transferToUserGroupId, transferToUserGroupType } = req.query;
       const actionName = req.query.actionName as PageActionOnGroupDelete;
 
-      const transferGroupInfo = transferToUserGroupId != null ? {
-        item: transferToUserGroupId as string,
-        type: GroupType.externalUserGroup,
-      } : undefined;
+      const transferToUserGroup = typeof transferToUserGroupId === 'string'
+        && (transferToUserGroupType === GroupType.userGroup || transferToUserGroupType === GroupType.externalUserGroup)
+        ? {
+          item: transferToUserGroupId,
+          type: transferToUserGroupType,
+        } : undefined;
 
       try {
         const userGroups = await (crowi.userGroupService as UserGroupService)
-          .removeCompletelyByRootGroupId(deleteGroupId, actionName, req.user, transferGroupInfo, ExternalUserGroup, ExternalUserGroupRelation);
+          .removeCompletelyByRootGroupId(deleteGroupId, actionName, req.user, transferToUserGroup, ExternalUserGroup, ExternalUserGroupRelation);
 
         const parameters = { action: SupportedAction.ACTION_ADMIN_USER_GROUP_DELETE };
         activityEvent.emit('update', res.locals.activity._id, parameters);

+ 5 - 2
apps/app/src/interfaces/apiv3/page.ts

@@ -1,5 +1,5 @@
 import type {
-  IPageHasId, IRevisionHasId, ITag,
+  IPageHasId, IRevisionHasId, ITag, Origin,
 } from '@growi/core';
 
 import type { IOptionsForCreate, IOptionsForUpdate } from '../page';
@@ -12,6 +12,8 @@ export type IApiv3PageCreateParams = IOptionsForCreate & {
   body?: string,
   pageTags?: string[],
 
+  origin?: Origin,
+
   isSlackEnabled?: boolean,
   slackChannels?: string,
 };
@@ -24,9 +26,10 @@ export type IApiv3PageCreateResponse = {
 
 export type IApiv3PageUpdateParams = IOptionsForUpdate & {
   pageId: string,
-  revisionId: string,
+  revisionId?: string,
   body: string,
 
+  origin?: Origin,
   isSlackEnabled?: boolean,
   slackChannels?: string,
 };

+ 0 - 11
apps/app/src/interfaces/editor-settings.ts

@@ -1,11 +0,0 @@
-import { type EditorTheme, type KeyMapMode } from '@growi/editor';
-
-export const DEFAULT_KEYMAP = 'default';
-export const DEFAULT_THEME = 'defaultlight';
-
-export interface IEditorSettings {
-  theme: undefined | EditorTheme,
-  keymapMode: undefined | KeyMapMode,
-  styleActiveLine: boolean,
-  autoFormatMarkdownTable: boolean,
-}

+ 3 - 1
apps/app/src/interfaces/page.ts

@@ -1,5 +1,5 @@
 import type {
-  GroupType, IGrantedGroup, IPageHasId, Nullable, PageGrant,
+  GroupType, IGrantedGroup, IPageHasId, Nullable, PageGrant, Origin,
 } from '@growi/core';
 
 import type { IPageOperationProcessData } from './page-operation';
@@ -33,6 +33,7 @@ export type IDeleteManyPageApiv3Result = {
 };
 
 export type IOptionsForUpdate = {
+  origin?: Origin
   grant?: PageGrant,
   userRelatedGrantUserGroupIds?: IGrantedGroup[],
   // isSyncRevisionToHackmd?: boolean,
@@ -44,5 +45,6 @@ export type IOptionsForCreate = {
   grantUserGroupIds?: IGrantedGroup[],
   overwriteScopesOfDescendants?: boolean,
 
+  origin?: Origin
   wip?: boolean,
 };

+ 1 - 1
apps/app/src/pages/installer.page.tsx

@@ -47,7 +47,7 @@ const InstallerPage: NextPage<Props> = (props: Props) => {
         i18n: t('installer.tab'),
       },
       external_accounts: {
-        Icon: () => <i className="icon-fw icon-share-alt"></i>,
+        Icon: () => <span className="growi-custom-icons">external_link</span>,
         Content: DataTransferForm,
         i18n: tCommons('g2g_data_transfer.tab'),
       },

+ 1 - 1
apps/app/src/pages/me/[[...path]].page.tsx

@@ -122,7 +122,7 @@ const MePage: NextPageWithLayout<Props> = (props: Props) => {
       <Head>
         <title>{title}</title>
       </Head>
-      <div className="dynamic-layout-root">
+      <div className="dynamic-layout-root mx-md-3">
         <header className="py-3">
           <div className="container">
             <h1 className="title fs-3 mt-5">{ targetPage.title }</h1>

+ 4 - 4
apps/app/src/server/models/editor-settings.ts

@@ -1,13 +1,13 @@
+import type { EditorSettings } from '@growi/editor';
+import type { Model, Document } from 'mongoose';
 import {
-  Schema, Model, Document,
+  Schema,
 } from 'mongoose';
 
-import { IEditorSettings } from '~/interfaces/editor-settings';
-
 import { getOrCreateModel } from '../util/mongoose-utils';
 
 
-export interface EditorSettingsDocument extends IEditorSettings, Document {
+export interface EditorSettingsDocument extends EditorSettings, Document {
   userId: Schema.Types.ObjectId,
 }
 export type EditorSettingsModel = Model<EditorSettingsDocument>

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

@@ -1,4 +1,4 @@
-import { PageGrant, GroupType } from '@growi/core';
+import { GroupType, Origin } from '@growi/core';
 import { templateChecker, pagePathUtils, pathUtils } from '@growi/core/dist/utils';
 import escapeStringRegexp from 'escape-string-regexp';
 
@@ -141,7 +141,14 @@ export const getPageSchema = (crowi) => {
     return relations.map((relation) => { return relation.relatedTag.name });
   };
 
-  pageSchema.methods.isUpdatable = function(previousRevision) {
+  pageSchema.methods.isUpdatable = async function(previousRevision, origin) {
+    const populatedPageDataWithRevisionOrigin = await this.populate('revision', 'origin');
+    const latestRevisionOrigin = populatedPageDataWithRevisionOrigin.revision.origin;
+    const ignoreLatestRevision = origin === Origin.Editor && (latestRevisionOrigin === Origin.Editor || latestRevisionOrigin === Origin.View);
+    if (ignoreLatestRevision) {
+      return true;
+    }
+
     const revision = this.latestRevision || this.revision;
     // comparing ObjectId with string
     // eslint-disable-next-line eqeqeq

+ 5 - 1
apps/app/src/server/models/revision.js

@@ -1,3 +1,5 @@
+import { allOrigin } from '@growi/core';
+
 import loggerFactory from '~/utils/logger';
 
 // disable no-return-await for model functions
@@ -29,6 +31,7 @@ module.exports = function(crowi) {
     format: { type: String, default: 'markdown' },
     author: { type: ObjectId, ref: 'User' },
     hasDiffToPrev: { type: Boolean },
+    origin: { type: String, enum: allOrigin },
   }, {
     timestamps: { createdAt: true, updatedAt: false },
   });
@@ -38,7 +41,7 @@ module.exports = function(crowi) {
     return this.updateMany({ pageId }, { $set: updateData });
   };
 
-  revisionSchema.statics.prepareRevision = function(pageData, body, previousBody, user, options) {
+  revisionSchema.statics.prepareRevision = function(pageData, body, previousBody, user, origin, options) {
     const Revision = this;
 
     if (!options) {
@@ -56,6 +59,7 @@ module.exports = function(crowi) {
     newRevision.body = body;
     newRevision.format = format;
     newRevision.author = user._id;
+    newRevision.origin = origin;
     if (pageData.revision != null) {
       newRevision.hasDiffToPrev = body !== previousBody;
     }

+ 4 - 2
apps/app/src/server/routes/apiv3/page/create-page.ts

@@ -1,3 +1,4 @@
+import { allOrigin } from '@growi/core';
 import type {
   IPage, IUser, IUserHasId,
 } from '@growi/core';
@@ -117,6 +118,7 @@ export const createPageHandlersFactory: CreatePageHandlersFactory = (crowi) => {
     body('isSlackEnabled').optional().isBoolean().withMessage('isSlackEnabled must be boolean'),
     body('slackChannels').optional().isString().withMessage('slackChannels must be string'),
     body('wip').optional().isBoolean().withMessage('wip must be boolean'),
+    body('origin').optional().isIn(allOrigin).withMessage('origin must be "view" or "editor"'),
   ];
 
 
@@ -227,10 +229,10 @@ export const createPageHandlersFactory: CreatePageHandlersFactory = (crowi) => {
       let createdPage;
       try {
         const {
-          grant, grantUserGroupIds, overwriteScopesOfDescendants, wip,
+          grant, grantUserGroupIds, overwriteScopesOfDescendants, wip, origin,
         } = req.body;
 
-        const options: IOptionsForCreate = { overwriteScopesOfDescendants, wip };
+        const options: IOptionsForCreate = { overwriteScopesOfDescendants, wip, origin };
         if (grant != null) {
           options.grant = grant;
           options.grantUserGroupIds = grantUserGroupIds;

+ 12 - 6
apps/app/src/server/routes/apiv3/page/update-page.ts

@@ -1,3 +1,4 @@
+import { allOrigin } from '@growi/core';
 import type {
   IPage, IRevisionHasId, IUserHasId,
 } from '@growi/core';
@@ -8,7 +9,7 @@ import { body } from 'express-validator';
 import mongoose from 'mongoose';
 
 import { SupportedAction, SupportedTargetModel } from '~/interfaces/activity';
-import type { IApiv3PageUpdateParams } from '~/interfaces/apiv3';
+import { type IApiv3PageUpdateParams } from '~/interfaces/apiv3';
 import type { IOptionsForUpdate } from '~/interfaces/page';
 import { RehypeSanitizeOption } from '~/interfaces/rehype';
 import type Crowi from '~/server/crowi';
@@ -63,7 +64,8 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
   const validator: ValidationChain[] = [
     body('pageId').exists().not().isEmpty({ ignore_whitespace: true })
       .withMessage("'pageId' must be specified"),
-    body('revisionId').exists().not().isEmpty({ ignore_whitespace: true })
+    body('revisionId').optional().exists().not()
+      .isEmpty({ ignore_whitespace: true })
       .withMessage("'revisionId' must be specified"),
     body('body').exists().isString()
       .withMessage("The empty value is not allowd for the 'body'"),
@@ -72,6 +74,7 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
     body('overwriteScopesOfDescendants').optional().isBoolean().withMessage('overwriteScopesOfDescendants must be boolean'),
     body('isSlackEnabled').optional().isBoolean().withMessage('isSlackEnabled must be boolean'),
     body('slackChannels').optional().isString().withMessage('slackChannels must be string'),
+    body('origin').optional().isIn(allOrigin).withMessage('origin must be "view" or "editor"'),
   ];
 
 
@@ -101,7 +104,8 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
     const { revisionId, isSlackEnabled, slackChannels } = req.body;
     if (isSlackEnabled) {
       try {
-        const results = await crowi.userNotificationService.fire(updatedPage, req.user, slackChannels, 'update', { previousRevision: revisionId });
+        const option = revisionId != null ? { previousRevision: revisionId } : undefined;
+        const results = await crowi.userNotificationService.fire(updatedPage, req.user, slackChannels, 'update', option);
         results.forEach((result) => {
           if (result.status === 'rejected') {
             logger.error('Create user notification failed', result.reason);
@@ -120,7 +124,9 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
     accessTokenParser, loginRequiredStrictly, excludeReadOnlyUser, addActivity,
     validator, apiV3FormValidator,
     async(req: UpdatePageRequest, res: ApiV3Response) => {
-      const { pageId, revisionId, body } = req.body;
+      const {
+        pageId, revisionId, body, origin,
+      } = req.body;
 
       // check page existence
       const isExist = await Page.count({ _id: pageId }) > 0;
@@ -130,7 +136,7 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
 
       // check revision
       const currentPage = await Page.findByIdAndViewer(pageId, req.user);
-      if (currentPage != null && !currentPage.isUpdatable(revisionId)) {
+      if (currentPage != null && !currentPage.isUpdatable(revisionId, origin)) {
         const latestRevision = await Revision.findById(currentPage.revision).populate('author');
         const returnLatestRevision = {
           revisionId: latestRevision?._id.toString(),
@@ -146,7 +152,7 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
       let updatedPage;
       try {
         const { grant, userRelatedGrantUserGroupIds, overwriteScopesOfDescendants } = req.body;
-        const options: IOptionsForUpdate = { overwriteScopesOfDescendants };
+        const options: IOptionsForUpdate = { overwriteScopesOfDescendants, origin };
         if (grant != null) {
           options.grant = grant;
           options.userRelatedGrantUserGroupIds = userRelatedGrantUserGroupIds;

+ 8 - 6
apps/app/src/server/routes/apiv3/user-group.js

@@ -429,15 +429,17 @@ module.exports = (crowi) => {
    */
   router.delete('/:id', loginRequiredStrictly, adminRequired, validator.delete, apiV3FormValidator, addActivity, async(req, res) => {
     const { id: deleteGroupId } = req.params;
-    const { actionName, transferToUserGroupId } = req.query;
+    const { actionName, transferToUserGroupId, transferToUserGroupType } = req.query;
 
-    const transferGroupInfo = transferToUserGroupId != null ? {
-      item: transferToUserGroupId,
-      type: GroupType.userGroup,
-    } : undefined;
+    const transferToUserGroup = typeof transferToUserGroupId === 'string'
+        && (transferToUserGroupType === GroupType.userGroup || transferToUserGroupType === GroupType.externalUserGroup)
+      ? {
+        item: transferToUserGroupId,
+        type: transferToUserGroupType,
+      } : undefined;
 
     try {
-      const userGroups = await crowi.userGroupService.removeCompletelyByRootGroupId(deleteGroupId, actionName, req.user, transferGroupInfo);
+      const userGroups = await crowi.userGroupService.removeCompletelyByRootGroupId(deleteGroupId, actionName, req.user, transferToUserGroup);
 
       const parameters = { action: SupportedAction.ACTION_ADMIN_USER_GROUP_DELETE };
       activityEvent.emit('update', res.locals.activity._id, parameters);

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