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

Merge branch 'dev/7.0.x' into imprv/133910-141426-replace-spinner-pulse

Tatsuya Ise 2 лет назад
Родитель
Сommit
42fb64b3cc
100 измененных файлов с 513 добавлено и 513 удалено
  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. 0 0
      apps/app/_obsolete/src/components/PageEditor/AbstractEditor.tsx
  6. 0 0
      apps/app/_obsolete/src/components/PageEditor/Editor.module.scss
  7. 2 2
      apps/app/_obsolete/src/components/PageEditor/Editor.tsx
  8. 0 0
      apps/app/_obsolete/src/components/PageEditor/EditorIcon.jsx
  9. 0 0
      apps/app/_obsolete/src/components/PageEditor/PasteHelper.js
  10. 0 0
      apps/app/_obsolete/src/components/PageEditor/PreventMarkdownListInterceptor.js
  11. 0 0
      apps/app/_obsolete/src/components/PageEditor/TextAreaEditor.jsx
  12. 2 4
      apps/app/_obsolete/src/components/Sidebar/AppearanceModeDropdown.tsx
  13. 4 4
      apps/app/package.json
  14. 3 2
      apps/app/public/static/locales/en_US/admin.json
  15. 9 4
      apps/app/public/static/locales/en_US/translation.json
  16. 3 2
      apps/app/public/static/locales/ja_JP/admin.json
  17. 9 4
      apps/app/public/static/locales/ja_JP/translation.json
  18. 3 2
      apps/app/public/static/locales/zh_CN/admin.json
  19. 9 4
      apps/app/public/static/locales/zh_CN/translation.json
  20. 1 1
      apps/app/src/client/models/MarkdownTable.js
  21. 3 1
      apps/app/src/client/services/create-page/use-create-template-page.ts
  22. 2 0
      apps/app/src/client/services/side-effects/drawio-modal-launcher-for-view.ts
  23. 3 0
      apps/app/src/client/services/side-effects/handsontable-modal-launcher-for-view.ts
  24. 18 0
      apps/app/src/client/services/use-toastr-on-error.tsx
  25. 2 2
      apps/app/src/components/Admin/AdminHome/AdminHome.jsx
  26. 1 1
      apps/app/src/components/Admin/App/AppSettingsPageContents.tsx
  27. 2 2
      apps/app/src/components/Admin/App/MaskedInput.tsx
  28. 4 3
      apps/app/src/components/Admin/AuditLog/ActivityTable.tsx
  29. 3 2
      apps/app/src/components/Admin/AuditLog/AuditLogSettings.tsx
  30. 3 2
      apps/app/src/components/Admin/AuditLog/DateRangePicker.tsx
  31. 5 3
      apps/app/src/components/Admin/AuditLog/SelectActionDropdown.tsx
  32. 1 1
      apps/app/src/components/Admin/Customize/CustomizeNoscriptSetting.tsx
  33. 1 1
      apps/app/src/components/Admin/Customize/CustomizeScriptSetting.tsx
  34. 21 10
      apps/app/src/components/Admin/Customize/ThemeColorBox.tsx
  35. 2 2
      apps/app/src/components/Admin/ExportArchiveData/SelectCollectionsModal.tsx
  36. 2 2
      apps/app/src/components/Admin/G2GDataTransferExportForm.tsx
  37. 4 4
      apps/app/src/components/Admin/G2GDataTransferStatusIcon.tsx
  38. 2 2
      apps/app/src/components/Admin/ImportData/GrowiArchive/ImportForm.jsx
  39. 7 2
      apps/app/src/components/Admin/Notification/NotificationTypeIcon.tsx
  40. 1 1
      apps/app/src/components/Admin/Notification/UserTriggerNotification.jsx
  41. 7 7
      apps/app/src/components/Admin/Security/LdapAuthTest.tsx
  42. 2 2
      apps/app/src/components/Admin/Security/LocalSecuritySettingContents.jsx
  43. 1 1
      apps/app/src/components/Admin/Security/SamlSecuritySettingContents.jsx
  44. 4 4
      apps/app/src/components/Admin/Security/SecurityManagementContents.jsx
  45. 1 1
      apps/app/src/components/Admin/Security/SecuritySetting.jsx
  46. 4 4
      apps/app/src/components/Admin/SlackIntegration/Bridge.jsx
  47. 3 3
      apps/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx
  48. 2 2
      apps/app/src/components/Admin/SlackIntegration/WithProxyAccordions.jsx
  49. 44 39
      apps/app/src/components/Admin/UserGroup/UserGroupDeleteModal.tsx
  50. 19 4
      apps/app/src/components/Admin/UserGroup/UserGroupPage.tsx
  51. 1 1
      apps/app/src/components/Admin/UserGroup/UserGroupTable.tsx
  52. 15 5
      apps/app/src/components/Admin/UserGroupDetail/UserGroupDetailPage.tsx
  53. 8 4
      apps/app/src/components/Admin/Users/SortIcons.tsx
  54. 2 2
      apps/app/src/components/Admin/Users/UserMenu.module.scss
  55. 5 2
      apps/app/src/components/Admin/Users/UserMenu.tsx
  56. 1 1
      apps/app/src/components/Bookmarks/BookmarkFolderItemControl.tsx
  57. 1 1
      apps/app/src/components/Bookmarks/BookmarkFolderMenu.tsx
  58. 1 1
      apps/app/src/components/Bookmarks/BookmarkMoveToRootBtn.tsx
  59. 3 2
      apps/app/src/components/Common/CustomCopyToClipBoard.tsx
  60. 1 1
      apps/app/src/components/Common/Dropdown/PageItemControl.tsx
  61. 5 0
      apps/app/src/components/Common/PagePathHierarchicalLink/PagePathHierarchicalLink.module.scss
  62. 2 3
      apps/app/src/components/Common/PagePathHierarchicalLink/PagePathHierarchicalLink.tsx
  63. 2 2
      apps/app/src/components/Common/PagePathNav/PagePathNav.tsx
  64. 2 4
      apps/app/src/components/CustomNavigation/CustomNav.module.scss
  65. 1 1
      apps/app/src/components/CustomNavigation/CustomNav.tsx
  66. 0 19
      apps/app/src/components/Icons/CompressIcon.tsx
  67. 0 22
      apps/app/src/components/Icons/CreatePageIcon.tsx
  68. 0 19
      apps/app/src/components/Icons/ExpandIcon.tsx
  69. 3 21
      apps/app/src/components/Icons/FolderIcon.tsx
  70. 0 16
      apps/app/src/components/Icons/FolderPlusIcon.tsx
  71. 0 13
      apps/app/src/components/Icons/KeyboardReturnEnterIcon.tsx
  72. 0 20
      apps/app/src/components/Icons/MoonIcon.jsx
  73. 0 16
      apps/app/src/components/Icons/PagePreviewIcon.jsx
  74. 0 15
      apps/app/src/components/Icons/ReturnTopIcon.tsx
  75. 0 28
      apps/app/src/components/Icons/SunIcon.jsx
  76. 2 2
      apps/app/src/components/Me/ApiSettings.tsx
  77. 2 2
      apps/app/src/components/Me/AssociateModal.tsx
  78. 2 2
      apps/app/src/components/Me/ColorModeSettings.tsx
  79. 10 10
      apps/app/src/components/Me/ExternalAccountLinkedMe.jsx
  80. 3 4
      apps/app/src/components/Me/InAppNotificationSettings.tsx
  81. 6 6
      apps/app/src/components/Me/PasswordSettings.jsx
  82. 2 2
      apps/app/src/components/Me/ProfileImageSettings.tsx
  83. 9 9
      apps/app/src/components/Me/QuestionnaireSettings.tsx
  84. 2 2
      apps/app/src/components/Me/UISettings.tsx
  85. 4 0
      apps/app/src/components/Navbar/GrowiContextualSubNavigation.module.scss
  86. 3 3
      apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  87. 3 1
      apps/app/src/components/Navbar/PageEditorModeManager.tsx
  88. 3 1
      apps/app/src/components/PageComment/CommentEditor.tsx
  89. 4 2
      apps/app/src/components/PageControls/LikeButtons.tsx
  90. 2 1
      apps/app/src/components/PageControls/SeenUserInfo.tsx
  91. 1 1
      apps/app/src/components/PageControls/_button-styles.scss
  92. 3 10
      apps/app/src/components/PageControls/user-list-popover.module.scss
  93. 59 90
      apps/app/src/components/PageCreateModal.tsx
  94. 1 1
      apps/app/src/components/PageEditor/Cheatsheet.tsx
  95. 5 0
      apps/app/src/components/PageEditor/EditorNavbar/EditingUserList.module.scss
  96. 57 0
      apps/app/src/components/PageEditor/EditorNavbar/EditingUserList.tsx
  97. 3 0
      apps/app/src/components/PageEditor/EditorNavbar/EditorNavbar.module.scss
  98. 22 0
      apps/app/src/components/PageEditor/EditorNavbar/EditorNavbar.tsx
  99. 1 0
      apps/app/src/components/PageEditor/EditorNavbar/index.ts
  100. 1 1
      apps/app/src/components/PageEditor/EditorNavbarBottom.tsx

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

+ 0 - 0
apps/app/src/components/PageEditor/AbstractEditor.tsx → apps/app/_obsolete/src/components/PageEditor/AbstractEditor.tsx


+ 0 - 0
apps/app/src/components/PageEditor/Editor.module.scss → apps/app/_obsolete/src/components/PageEditor/Editor.module.scss


+ 2 - 2
apps/app/src/components/PageEditor/Editor.tsx → 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,

+ 0 - 0
apps/app/src/components/PageEditor/EditorIcon.jsx → apps/app/_obsolete/src/components/PageEditor/EditorIcon.jsx


+ 0 - 0
apps/app/src/components/PageEditor/PasteHelper.js → apps/app/_obsolete/src/components/PageEditor/PasteHelper.js


+ 0 - 0
apps/app/src/components/PageEditor/PreventMarkdownListInterceptor.js → apps/app/_obsolete/src/components/PageEditor/PreventMarkdownListInterceptor.js


+ 0 - 0
apps/app/src/components/PageEditor/TextAreaEditor.jsx → apps/app/_obsolete/src/components/PageEditor/TextAreaEditor.jsx


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

+ 4 - 4
apps/app/package.json

@@ -101,7 +101,7 @@
     "connect-redis": "^4.0.4",
     "cookie-parser": "^1.4.5",
     "csurf": "^1.11.0",
-    "csv-to-markdown-table": "^1.1.0",
+    "csv-to-markdown-table": "^1.4.1",
     "date-fns": "^2.23.0",
     "dayjs": "^1.11.7",
     "detect-indent": "^7.0.0",
@@ -131,7 +131,7 @@
     "is-iso-date": "^0.0.1",
     "ldapjs": "^3.0.2",
     "lucene-query-parser": "^1.2.0",
-    "markdown-table": "^1.1.1",
+    "markdown-table": "^3.0.3",
     "md5": "^2.2.1",
     "mermaid": "^10.1.0",
     "method-override": "^3.0.0",
@@ -214,7 +214,7 @@
     "xss": "^1.0.14",
     "y-mongodb-provider": "^0.1.7",
     "y-socket.io": "^1.1.0",
-    "yjs": "^13.6.7"
+    "yjs": "^13.6.12"
   },
   "// comments for defDependencies": {
     "@handsontable/react": "v3 requires handsontable >= 7.0.0.",
@@ -268,7 +268,7 @@
     "pretty-bytes": "^6.1.1",
     "react-codemirror2": "^6.0.0",
     "react-copy-to-clipboard": "^5.0.1",
-    "react-dropzone": "^11.2.4",
+    "react-dropzone": "^14.2.3",
     "react-hotkeys": "^2.0.0",
     "react-input-autosize": "^3.0.0",
     "rehype-rewrite": "^3.0.6",

+ 3 - 2
apps/app/public/static/locales/en_US/admin.json

@@ -834,9 +834,10 @@
       "dropdown_desc": "Choose an action for private pages",
       "select_group": "Select a group",
       "no_groups": "No groups to select",
-      "publish_pages": "Publish all",
+      "publish_pages": "Publish pages that are publishable",
       "delete_pages": "Delete all",
-      "transfer_pages": "Transfer to another group"
+      "transfer_pages": "Transfer to another group",
+      "option_explanation": "A \"publishable\" page is a page visible only to the group you want to delete. Pages that can be viewed by other groups will not be published."
     },
     "update_parent_confirm_modal": {
       "header": "The parent of the group will be changed",

+ 9 - 4
apps/app/public/static/locales/en_US/translation.json

@@ -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",
@@ -311,6 +311,11 @@
     }
   },
   "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",
@@ -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",
@@ -828,10 +833,10 @@
     "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"

+ 3 - 2
apps/app/public/static/locales/ja_JP/admin.json

@@ -844,9 +844,10 @@
       "dropdown_desc": "削除するグループの限定公開ページの処理を選択してください",
       "select_group": "グループを選択してください",
       "no_groups": "グループがありません",
-      "publish_pages": "全て公開する",
+      "publish_pages": "公開可能なページを公開する",
       "delete_pages": "全て削除する",
-      "transfer_pages": "全て他のグループに移譲する"
+      "transfer_pages": "全て他のグループに移譲する",
+      "option_explanation": "「公開可能なページ」とは、削除するグループにのみ限定公開されているページを指します。他のグループも閲覧可能なページは公開対象となりません。"
     },
     "update_parent_confirm_modal": {
       "header": "グループの親が変更されます",

+ 9 - 4
apps/app/public/static/locales/ja_JP/translation.json

@@ -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": "基本設定",
@@ -344,6 +344,11 @@
     }
   },
   "page_edit": {
+    "input_channels": "チャンネル名",
+    "theme": "テーマ",
+    "keymap": "キーマップ",
+    "indent": "インデント",
+    "editor_config": "エディタ設定",
     "Show active line": "アクティブ行をハイライト",
     "auto_format_table": "表の自動整形",
     "overwrite_scopes": "{{operation}}と同時に全ての配下ページのスコープを上書き",
@@ -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": "以下のページを変換します",
@@ -861,10 +866,10 @@
     "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 を解除できませんでした"

+ 3 - 2
apps/app/public/static/locales/zh_CN/admin.json

@@ -843,9 +843,10 @@
       "dropdown_desc": "为私人页选择操作",
       "select_group": "选择组",
       "no_groups": "没有可选择的组",
-      "publish_pages": "全部发布",
+      "publish_pages": "发布可以发布的页面",
       "delete_pages": "全部删除",
-      "transfer_pages": "转移到另一组"
+      "transfer_pages": "转移到另一组",
+      "option_explanation": "\"可发布页面\"是指仅对您要删除的群组可见的页面。其他群组可以查看的页面将不会被发布。"
     },
     "update_parent_confirm_modal": {
       "header": "该组的父组被改变",

+ 9 - 4
apps/app/public/static/locales/zh_CN/translation.json

@@ -117,7 +117,7 @@
 	"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": "用户组",
@@ -301,6 +301,11 @@
 		}
 	},
 	"page_edit": {
+    "input_channels": "频道名",
+    "theme": "主题",
+    "keymap": "键表",
+    "indent": "缩进",
+    "editor_config": "编辑器配置",
 		"Show active line": "显示活动行",
 		"auto_format_table": "自动格式化表格",
 		"overwrite_scopes": "{{operation}和覆盖所有子体的作用域",
@@ -557,7 +562,7 @@
     "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": "转换页面",
@@ -831,10 +836,10 @@
     "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"

+ 1 - 1
apps/app/src/client/models/MarkdownTable.js

@@ -1,5 +1,5 @@
 import csvToMarkdown from 'csv-to-markdown-table';
-import markdownTable from 'markdown-table';
+import { markdownTable } from 'markdown-table';
 import stringWidth from 'string-width';
 
 // https://github.com/markdown-it/markdown-it/blob/d29f421927e93e88daf75f22089a3e732e195bd2/lib/rules_block/table.js#L83

+ 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?.();

+ 18 - 0
apps/app/src/client/services/use-toastr-on-error.tsx

@@ -0,0 +1,18 @@
+import { useCallback } from 'react';
+
+import { useTranslation } from 'react-i18next';
+
+import { toastError } from '~/client/util/toastr';
+
+export const useToastrOnError = <P, R>(method?: (param?: P) => Promise<R|undefined>): (param?: P) => Promise<R|undefined> => {
+  const { t } = useTranslation('commons');
+
+  return useCallback(async(param) => {
+    try {
+      return await method?.(param);
+    }
+    catch (err) {
+      toastError(t('toaster.create_failed', { target: 'a page' }));
+    }
+  }, [method, t]);
+};

+ 2 - 2
apps/app/src/components/Admin/AdminHome/AdminHome.jsx

@@ -52,7 +52,7 @@ const AdminHome = (props) => {
             </p>
             <hr />
             <a className="btn-link" href="/admin/app" rel="noopener noreferrer">
-              <i className="fa fa-link ms-1" aria-hidden="true"></i>
+              <span className="material-symbols-outlined ms-1" aria-hidden="true">link</span>
               <strong>{t('admin:maintenance_mode.end_maintenance_mode')}</strong>
             </a>
           </div>
@@ -65,7 +65,7 @@ const AdminHome = (props) => {
           <div className={`alert ${migrationStatus.isV5Compatible == null ? 'alert-warning' : 'alert-info'}`}>
             {t('admin:v5_page_migration.migration_desc')}
             <a className="btn-link" href="/admin/app" rel="noopener noreferrer">
-              <i className="fa fa-link ms-1" aria-hidden="true"></i>
+              <span className="material-symbols-outlined ms-1" aria-hidden="true">link</span>
               <strong>{t('admin:v5_page_migration.upgrade_to_v5')}</strong>
             </a>
           </div>

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

@@ -62,7 +62,7 @@ const AppSettingsPageContents = (props: Props) => {
             </p>
             <hr />
             <a className="btn-link" href="#maintenance-mode" rel="noopener noreferrer">
-              <i className="fa fa-fw fa-arrow-down ms-1" aria-hidden="true"></i>
+              <span className="material-symbols-outlined ms-1" aria-hidden="true">expand_more</span>
               <strong>{t('admin:maintenance_mode.end_maintenance_mode')}</strong>
             </a>
           </div>

+ 2 - 2
apps/app/src/components/Admin/App/MaskedInput.tsx

@@ -33,9 +33,9 @@ export default function MaskedInput(props: Props): JSX.Element {
       />
       <span onClick={togglePassword} className={styles.PasswordReveal}>
         {passwordShown ? (
-          <i className="fa fa-eye" />
+          <span className="material-symbols-outlined">visibility</span>
         ) : (
-          <i className="fa fa-eye-slash" />
+          <span className="material-symbols-outlined">visibility_off</span>
         )}
       </span>
     </div>

+ 4 - 3
apps/app/src/components/Admin/AuditLog/ActivityTable.tsx

@@ -1,4 +1,5 @@
-import React, { FC, useState, useCallback } from 'react';
+import type { FC } from 'react';
+import React, { useState, useCallback } from 'react';
 
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { UserPicture } from '@growi/ui/dist/components';
@@ -7,7 +8,7 @@ import { CopyToClipboard } from 'react-copy-to-clipboard';
 import { useTranslation } from 'react-i18next';
 import { Tooltip } from 'reactstrap';
 
-import { IActivityHasId } from '~/interfaces/activity';
+import type { IActivityHasId } from '~/interfaces/activity';
 
 type Props = {
   activityList: IActivityHasId[]
@@ -64,7 +65,7 @@ export const ActivityTable : FC<Props> = (props: Props) => {
                   {activity.endpoint}
                   <CopyToClipboard text={activity.endpoint} onCopy={showToolTip}>
                     <button type="button" className="btn btn-outline-secondary border-0 pull-right" id="tooltipTarget">
-                      <i className="fa fa-clipboard" aria-hidden="true"></i>
+                      <span className="material-symbols-outlined" aria-hidden="true">content_paste</span>
                     </button>
                   </CopyToClipboard>
                   <Tooltip placement="top" isOpen={tooltopOpen} fade={false} target="tooltipTarget">

+ 3 - 2
apps/app/src/components/Admin/AuditLog/AuditLogSettings.tsx

@@ -1,4 +1,5 @@
-import React, { FC, useState } from 'react';
+import type { FC } from 'react';
+import React, { useState } from 'react';
 
 import { useTranslation } from 'react-i18next';
 import { Collapse } from 'reactstrap';
@@ -54,7 +55,7 @@ export const AuditLogSettings: FC = () => {
       </p>
       <p className="mt-1">
         <button type="button" className="btn btn-link p-0" aria-expanded="false" onClick={() => setIsExpandActionList(!isExpandActionList)}>
-          <i className={`fa fa-fw fa-arrow-right ${isExpandActionList ? 'fa-rotate-90' : ''}`}></i>
+          <span className={`material-symbols-outlined me-1 ${isExpandActionList ? 'fa-rotate-90' : ''}`}>navigate_next</span>
           { t('admin:audit_log_management.action_list') }
         </button>
       </p>

+ 3 - 2
apps/app/src/components/Admin/AuditLog/DateRangePicker.tsx

@@ -1,4 +1,5 @@
-import React, { FC, forwardRef, useCallback } from 'react';
+import type { FC } from 'react';
+import React, { forwardRef, useCallback } from 'react';
 
 import { addDays, format } from 'date-fns';
 import DatePicker from 'react-datepicker';
@@ -19,7 +20,7 @@ const CustomInput = forwardRef<HTMLInputElement, CustomInputProps>((props: Custo
   return (
     <div className="input-group admin-audit-log">
       <span className="input-group-text">
-        <i className="fa fa-fw fa-calendar" />
+        <span className="material-symbols-outlined me-1">calendar_month</span>
       </span>
       <input
         ref={ref}

+ 5 - 3
apps/app/src/components/Admin/AuditLog/SelectActionDropdown.tsx

@@ -1,9 +1,11 @@
-import React, { FC, useMemo, useCallback } from 'react';
+import type { FC } from 'react';
+import React, { useMemo, useCallback } from 'react';
 
 import { useTranslation } from 'react-i18next';
 
+import type { SupportedActionType, SupportedActionCategoryType } from '~/interfaces/activity';
 import {
-  SupportedActionType, SupportedActionCategoryType, SupportedActionCategory,
+  SupportedActionCategory,
   PageActions, CommentActions, TagActions, ShareLinkActions, AttachmentActions, InAppNotificationActions, SearchActions, UserActions, AdminActions,
 } from '~/interfaces/activity';
 
@@ -78,7 +80,7 @@ export const SelectActionDropdown: FC<Props> = (props: Props) => {
   return (
     <div className="btn-group me-2 admin-audit-log">
       <button className="btn btn-outline-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-bs-toggle="dropdown">
-        <i className="fa fa-fw fa-bolt" />{t('admin:audit_log_management.action')}
+        <span className="material-symbols-outlined me-1">bolt</span>{t('admin:audit_log_management.action')}
       </button>
       <ul className="dropdown-menu select-action-dropdown" aria-labelledby="dropdownMenuButton">
         {dropdownItems.map(item => (

+ 1 - 1
apps/app/src/components/Admin/Customize/CustomizeNoscriptSetting.tsx

@@ -69,7 +69,7 @@ const CustomizeNoscriptSetting = (props: Props): JSX.Element => {
             aria-expanded="false"
             aria-controls="collapseExampleHtml"
           >
-            <i className="fa fa-fw fa-chevron-right" aria-hidden="true"></i>
+            <span className="material-symbols-outlined me-1" aria-hidden="true">navigate_next</span>
             Example for Google Tag Manager
           </a>
           <div className="collapse" id="collapseExampleHtml">

+ 1 - 1
apps/app/src/components/Admin/Customize/CustomizeScriptSetting.tsx

@@ -66,7 +66,7 @@ const CustomizeScriptSetting = (props: Props): JSX.Element => {
             aria-expanded="false"
             aria-controls="collapseExampleScript"
           >
-            <i className="fa fa-fw fa-chevron-right" aria-hidden="true"></i>
+            <span className="material-symbols-outlined me-1" aria-hidden="true">navigate_next</span>
             Example for Google Tag Manager
           </a>
           <div className="collapse" id="collapseExampleScript">

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

+ 2 - 2
apps/app/src/components/Admin/ExportArchiveData/SelectCollectionsModal.tsx

@@ -166,10 +166,10 @@ const SelectCollectionsModal = (props: Props): JSX.Element => {
           <div className="row">
             <div className="col-sm-12">
               <button type="button" className="btn btn-sm btn-outline-secondary me-2" onClick={checkAll}>
-                <i className="fa fa-check-square-o"></i> {t('admin:export_management.check_all')}
+                <span className="material-symbols-outlined">check_box</span> {t('admin:export_management.check_all')}
               </button>
               <button type="button" className="btn btn-sm btn-outline-secondary me-2" onClick={uncheckAll}>
-                <i className="fa fa-square-o"></i> {t('admin:export_management.uncheck_all')}
+                <span className="material-symbols-outlined">check_box_outline_blank</span> {t('admin:export_management.uncheck_all')}
               </button>
             </div>
           </div>

+ 2 - 2
apps/app/src/components/Admin/G2GDataTransferExportForm.tsx

@@ -202,12 +202,12 @@ const G2GDataTransferExportForm = (props: Props): JSX.Element => {
       <form className="mt-3 row row-cols-lg-auto g-3 align-items-center">
         <div className="col-12">
           <button type="button" className="btn btn-sm btn-outline-secondary me-2" onClick={checkAll}>
-            <i className="fa fa-check-square-o"></i> {t('admin:export_management.check_all')}
+            <span className="material-symbols-outlined">check_box</span>, {t('admin:export_management.check_all')}
           </button>
         </div>
         <div className="col-12">
           <button type="button" className="btn btn-sm btn-outline-secondary me-2" onClick={uncheckAll}>
-            <i className="fa fa-square-o"></i> {t('admin:export_management.uncheck_all')}
+            <span className="material-symbols-outlined">check_box_outline_blank</span> {t('admin:export_management.uncheck_all')}
           </button>
         </div>
       </form>

+ 4 - 4
apps/app/src/components/Admin/G2GDataTransferStatusIcon.tsx

@@ -22,23 +22,23 @@ const G2GDataTransferStatusIcon = ({ status, className, ...props }: Props): JSX.
 
   if (status === G2G_PROGRESS_STATUS.COMPLETED) {
     return (
-      <i className={`fa fa-check-circle-o fa-fw text-info ${className}`} aria-label="completed" {...props} />
+      <span className={`material-symbols-outlined text-info ${className}`} aria-label="completed" {...props}>check_circle</span>
     );
   }
 
   if (status === G2G_PROGRESS_STATUS.ERROR) {
     return (
-      <i className={`fa fa-exclamation-circle fa-fw text-danger ${className}`} aria-label="error" {...props} />
+      <span className={`material-symbols-outlined text-danger ${className}`} aria-label="error" {...props}>error</span>
     );
   }
 
   if (status === G2G_PROGRESS_STATUS.SKIPPED) {
     return (
-      <i className={`fa fa-ban fa-fw ${className}`} aria-label="skipped" {...props} />
+      <span className={`material-symbols-outlined ${className}`} aria-label="skipped" {...props}>block</span>
     );
   }
 
-  return <i className={`fa fa-circle-o fa-fw ${className}`} aria-label="pending" {...props} />;
+  return <span className={`material-symbols-outlined ${className}`} aria-label="pending" {...props}>circle</span>;
 };
 
 export default G2GDataTransferStatusIcon;

+ 2 - 2
apps/app/src/components/Admin/ImportData/GrowiArchive/ImportForm.jsx

@@ -449,12 +449,12 @@ class ImportForm extends React.Component {
         <form className="row row-cols-lg-auto g-3 align-items-center">
           <div className="col-12">
             <button type="button" className="btn btn-sm btn-outline-secondary me-2" onClick={this.checkAll}>
-              <i className="fa fa-check-square-o"></i> {t('admin:export_management.check_all')}
+              <span className="material-symbols-outlined">check_box</span> {t('admin:export_management.check_all')}
             </button>
           </div>
           <div className="col-12">
             <button type="button" className="btn btn-sm btn-outline-secondary me-2" onClick={this.uncheckAll}>
-              <i className="fa fa-square-o"></i> {t('admin:export_management.uncheck_all')}
+              <span className="material-symbols-outlined">check_box_outline_blank</span> {t('admin:export_management.uncheck_all')}
             </button>
           </div>
         </form>

+ 7 - 2
apps/app/src/components/Admin/Notification/NotificationTypeIcon.tsx

@@ -23,8 +23,13 @@ export const NotificationTypeIcon = (props: NotificationTypeIconProps): JSX.Elem
   }
 
   const elemId = `notification-${type}-${_id}`;
-  const className = type === 'mail' ? 'icon-fw fa fa-envelope-o' : 'icon-fw fa fa-hashtag';
+  const iconName = type === 'mail' ? 'mail' : 'tag';
   const toolChip = type === 'mail' ? 'Mail' : 'Slack';
 
-  return <><i id={elemId} className={className}></i><UncontrolledTooltip target={elemId}>{toolChip}</UncontrolledTooltip></>;
+  return (
+    <>
+      <span id={elemId} className="material-symbols-outlined me-1">{iconName}</span>
+      <UncontrolledTooltip target={elemId}>{toolChip}</UncontrolledTooltip>
+    </>
+  );
 };

+ 1 - 1
apps/app/src/components/Admin/Notification/UserTriggerNotification.jsx

@@ -112,7 +112,7 @@ class UserTriggerNotification extends React.Component {
               <td>
                 <div className="input-group notify-to-option" id="slack-input">
                   <div>
-                    <span className="input-group-text"><i className="fa fa-hashtag" /></span>
+                    <span className="input-group-text"><span className="material-symbols-outlined">tag</span></span>
                   </div>
                   <input
                     className="form-control"

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

+ 2 - 2
apps/app/src/components/Admin/Security/LocalSecuritySettingContents.jsx

@@ -181,7 +181,7 @@ class LocalSecuritySettingContents extends React.Component {
                   <div className="alert alert-warning p-1 my-1 small d-inline-block">
                     <span>{t('commons:alert.password_reset_please_enable_mailer')}</span>
                     <Link href="/admin/app#mail-settings">
-                      <i className="fa fa-link"></i> {t('app_setting.mail_settings')}
+                      <span className="material-symbols-outlined">link</span> {t('app_setting.mail_settings')}
                     </Link>
                   </div>
                 )}
@@ -210,7 +210,7 @@ class LocalSecuritySettingContents extends React.Component {
                   <div className="alert alert-warning p-1 my-1 small d-inline-block">
                     <span>{t('commons:alert.please_enable_mailer')}</span>
                     <Link href="/admin/app#mail-settings">
-                      <i className="fa fa-link"></i> {t('app_setting.mail_settings')}
+                      <span className="material-symbols-outlined">link</span> {t('app_setting.mail_settings')}
                     </Link>
                   </div>
                 )}

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

+ 4 - 4
apps/app/src/components/Admin/Security/SecurityManagementContents.jsx

@@ -30,19 +30,19 @@ const SecurityManagementContents = () => {
   const navTabMapping = useMemo(() => {
     return {
       passport_local: {
-        Icon: () => <i className="fa fa-users" />,
+        Icon: () => <span className="material-symbols-outlined">groups</span>,
         i18n: 'ID/Pass',
       },
       passport_ldap: {
-        Icon: () => <i className="fa fa-sitemap" />,
+        Icon: () => <span className="material-symbols-outlined">network_node</span>,
         i18n: 'LDAP',
       },
       passport_saml: {
-        Icon: () => <i className="fa fa-key" />,
+        Icon: () => <span className="material-symbols-outlined">key</span>,
         i18n: 'SAML',
       },
       passport_oidc: {
-        Icon: () => <i className="fa fa-key" />,
+        Icon: () => <span className="material-symbols-outlined">key</span>,
         i18n: 'OIDC',
       },
       passport_google: {

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

@@ -296,7 +296,7 @@ class SecuritySetting extends React.Component {
                     aria-expanded="false"
                     onClick={() => this.setExpantOtherDeleteOptionsState(deletionType, !expantDeleteOptionsState)}
                   >
-                    <i className={`fa fa-fw fa-arrow-right ${expantDeleteOptionsState ? 'fa-rotate-90' : ''}`}></i>
+                    <span className={`material-symbols-outlined me-1 ${expantDeleteOptionsState ? 'fa-rotate-90' : ''}`}>navigate_next</span>
                     { t('security_settings.other_options') }
                   </button>
                   <Collapse isOpen={expantDeleteOptionsState}>

+ 4 - 4
apps/app/src/components/Admin/SlackIntegration/Bridge.jsx

@@ -72,15 +72,15 @@ const Bridge = (props) => {
   // all green
   else if (errorCount === 0) {
     description = t('admin:slack_integration.integration_sentence.integration_successful');
-    iconClass = 'fa fa-check text-success';
-    iconName = '';
+    iconClass = 'material-symbols-outlined text-success';
+    iconName = 'check';
     hrClass = 'border-success admin-border-success';
   }
   // some of them failed
   else {
     description = t('admin:slack_integration.integration_sentence.integration_some_ws_is_not_complete');
-    iconClass = 'fa fa-check text-warning';
-    iconName = '';
+    iconClass = 'material-symbols-outlined text-warning';
+    iconName = 'check';
     hrClass = 'border-warning admin-border-failed';
   }
 

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

@@ -115,7 +115,7 @@ const CustomBotWithoutProxySettingsAccordion = (props) => {
       <Accordion
         defaultIsActive={defaultOpenAccordionKeys.has(botInstallationStep.REGISTER_SLACK_CONFIGURATION)}
         // eslint-disable-next-line max-len
-        title={<><span className="me-3">3</span>{t('admin:slack_integration.accordion.register_secret_and_token')}{isEnterdSecretAndToken && <i className="ms-3 text-success fa fa-check"></i>}</>}
+        title={<><span className="me-3">3</span>{t('admin:slack_integration.accordion.register_secret_and_token')}{isEnterdSecretAndToken && <span className="material-symbols-outlined ms-3 text-success">check</span>}</>}
       >
         <CustomBotWithoutProxySecretTokenSection
           onUpdatedSecretToken={props.onUpdatedSecretToken}
@@ -138,7 +138,7 @@ const CustomBotWithoutProxySettingsAccordion = (props) => {
       <Accordion
         defaultIsActive={defaultOpenAccordionKeys.has(botInstallationStep.CONNECTION_TEST)}
         // eslint-disable-next-line max-len
-        title={<><span className="me-3">5</span>{t('admin:slack_integration.accordion.test_connection')}{isLatestConnectionSuccess && <i className="ms-3 text-success fa fa-check"></i>}</>}
+        title={<><span className="me-3">5</span>{t('admin:slack_integration.accordion.test_connection')}{isLatestConnectionSuccess && <span className="material-symbols-outlined ms-3 text-success">check</span>}</>}
       >
         <p className="text-center m-4">{t('admin:slack_integration.accordion.test_connection_by_pressing_button')}</p>
         <p className="text-center text-warning">
@@ -148,7 +148,7 @@ const CustomBotWithoutProxySettingsAccordion = (props) => {
           <form className="align-items-center" onSubmit={e => submitForm(e)}>
             <div className="input-group col-8">
               <div>
-                <span className="input-group-text" id="slack-channel-addon"><i className="fa fa-hashtag" /></span>
+                <span className="input-group-text" id="slack-channel-addon"><span className="material-symbols-outlined">tag</span></span>
               </div>
               <input
                 className="form-control"

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

@@ -246,7 +246,7 @@ const TestProcess = ({
         <form className="justify-content-center" onSubmit={e => submitForm(e)}>
           <div className="input-group col-8">
             <div>
-              <span className="input-group-text" id="slack-channel-addon"><i className="fa fa-hashtag" /></span>
+              <span className="input-group-text" id="slack-channel-addon"><span className="material-symbols-outlined">tag</span></span>
             </div>
             <input
               className="form-control"
@@ -391,7 +391,7 @@ const WithProxyAccordions = (props) => {
               <>
                 <span className="me-3">{key}</span>
                 {t(`admin:slack_integration.accordion.${value.title}`)}
-                {value.title === 'test_connection' && isLatestConnectionSuccess && <i className="ms-3 text-success fa fa-check"></i>}
+                {value.title === 'test_connection' && isLatestConnectionSuccess && <span className="material-symbols-outlined ms-3 text-success">check</span>}
               </>
             )}
             key={key}

+ 44 - 39
apps/app/src/components/Admin/UserGroup/UserGroupDeleteModal.tsx

@@ -1,13 +1,16 @@
-import React, {
-  FC, useCallback, useState, useMemo,
-} from 'react';
+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,
 } from 'reactstrap';
 
+import { PageActionOnGroupDelete } from '~/interfaces/user-group';
+
 
 /**
  * Delete User Group Select component
@@ -17,28 +20,21 @@ import {
  * @extends {React.Component}
  */
 type Props = {
-  userGroups: IUserGroupHasId[],
+  userGroups: IGrantedGroup[],
   deleteUserGroup?: IUserGroupHasId,
-  onDelete?: (deleteGroupId: string, actionName: string, transferToUserGroupId: string) => Promise<void> | void,
+  onDelete?: (deleteGroupId: string, actionName: PageActionOnGroupDelete, transferToUserGroup: IGrantedGroup | null) => Promise<void> | void,
   isShow: boolean,
   onHide?: () => Promise<void> | void,
 };
 
 type AvailableOption = {
   id: number,
-  actionForPages: string,
+  actionForPages: PageActionOnGroupDelete,
   iconClass: string,
   styleClass: string,
   label: string,
 };
 
-// actionName master constants
-const actionForPages = {
-  public: 'public',
-  delete: 'delete',
-  transfer: 'transfer',
-};
-
 export const UserGroupDeleteModal: FC<Props> = (props: Props) => {
 
   const { t } = useTranslation();
@@ -51,21 +47,21 @@ export const UserGroupDeleteModal: FC<Props> = (props: Props) => {
     return [
       {
         id: 1,
-        actionForPages: actionForPages.public,
+        actionForPages: PageActionOnGroupDelete.publicize,
         iconClass: 'icon-people',
         styleClass: '',
         label: t('admin:user_group_management.delete_modal.publish_pages'),
       },
       {
         id: 2,
-        actionForPages: actionForPages.delete,
+        actionForPages: PageActionOnGroupDelete.delete,
         iconClass: 'icon-trash',
         styleClass: 'text-danger',
         label: t('admin:user_group_management.delete_modal.delete_pages'),
       },
       {
         id: 3,
-        actionForPages: actionForPages.transfer,
+        actionForPages: PageActionOnGroupDelete.transfer,
         iconClass: 'icon-options',
         styleClass: '',
         label: t('admin:user_group_management.delete_modal.transfer_pages'),
@@ -76,15 +72,15 @@ export const UserGroupDeleteModal: FC<Props> = (props: Props) => {
   /*
    * State
    */
-  const [actionName, setActionName] = useState<string>('');
-  const [transferToUserGroupId, setTransferToUserGroupId] = useState<string>('');
+  const [actionName, setActionName] = useState<PageActionOnGroupDelete | null>(null);
+  const [transferToUserGroup, setTransferToUserGroup] = useState<IGrantedGroup | null>(null);
 
   /*
    * Function
    */
   const resetStates = useCallback(() => {
-    setActionName('');
-    setTransferToUserGroupId('');
+    setActionName(null);
+    setTransferToUserGroup(null);
   }, []);
 
   const toggleHandler = useCallback(() => {
@@ -103,11 +99,12 @@ 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) {
+    if (onDelete == null || deleteUserGroup == null || actionName == null) {
       return;
     }
 
@@ -116,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) => {
@@ -130,7 +127,7 @@ export const UserGroupDeleteModal: FC<Props> = (props: Props) => {
         name="actionName"
         className="form-control"
         placeholder="select"
-        value={actionName}
+        value={actionName ?? ''}
         onChange={handleActionChange}
       >
         <option value="" disabled>{t('admin:user_group_management.delete_modal.dropdown_desc')}</option>
@@ -145,41 +142,44 @@ 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"
-        className={`form-control ${actionName === actionForPages.transfer ? '' : 'd-none'}`}
-        value={transferToUserGroupId}
+        name="transferToUserGroup"
+        className={`form-control ${actionName === PageActionOnGroupDelete.transfer ? '' : 'd-none'}`}
+        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;
 
-    if (actionName === '') {
+    if (actionName === null) {
       isValid = false;
     }
-    else if (actionName === actionForPages.transfer) {
-      isValid = transferToUserGroupId !== '';
+    else if (actionName === PageActionOnGroupDelete.transfer) {
+      isValid = transferToUserGroup != null;
     }
 
     return isValid;
-  }, [actionName, transferToUserGroupId]);
+  }, [actionName, transferToUserGroup]);
 
   return (
     <Modal className="modal-md" isOpen={props.isShow} toggle={toggleHandler}>
@@ -196,7 +196,7 @@ export const UserGroupDeleteModal: FC<Props> = (props: Props) => {
       </ModalBody>
       <ModalFooter>
         <form className="d-flex justify-content-between w-100" onSubmit={handleSubmit}>
-          <div className="d-flex mb-0">
+          <div className="d-flex mb-0 me-3">
             {renderPageActionSelector()}
             {renderGroupSelector()}
           </div>
@@ -204,6 +204,11 @@ export const UserGroupDeleteModal: FC<Props> = (props: Props) => {
             <span className="material-symbols-outlined">delete_forever</span> {t('Delete')}
           </button>
         </form>
+        {actionName === PageActionOnGroupDelete.publicize && (
+          <div className="form-text text-muted">
+            <small>{t('admin:user_group_management.delete_modal.option_explanation')}</small>
+          </div>
+        )}
       </ModalFooter>
     </Modal>
   );

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

@@ -1,12 +1,17 @@
-import React, { FC, useState, useCallback } from 'react';
+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';
 
@@ -24,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);
@@ -126,11 +138,14 @@ export const UserGroupPage: FC = () => {
     }
   }, [t, mutateUserGroups, hideUpdateModal]);
 
-  const deleteUserGroupById = useCallback(async(deleteGroupId: string, actionName: string, 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
@@ -187,7 +202,7 @@ export const UserGroupPage: FC = () => {
       />
 
       <UserGroupDeleteModal
-        userGroups={userGroups}
+        userGroups={userGroupsForDeleteModal.concat(externalUserGroupsForDeleteModal)}
         deleteUserGroup={selectedUserGroup}
         onDelete={deleteUserGroupById}
         isShow={isDeleteModalShown}

+ 1 - 1
apps/app/src/components/Admin/UserGroup/UserGroupTable.tsx

@@ -214,7 +214,7 @@ export const UserGroupTable: FC<Props> = ({
                           {onRemove != null
                           && (
                             <button className="dropdown-item" type="button" role="button" onClick={onClickRemove} data-user-group-id={group._id}>
-                              <i className="icon-fw fa fa-chain-broken"></i> {t('admin:user_group_management.remove_child_group')}
+                              <span className="material-symbols-outlined me-1">group_remove</span> {t('admin:user_group_management.remove_child_group')}
                             </button>
                           )}
                           <button className="dropdown-item" type="button" role="button" onClick={onClickDelete} data-user-group-id={group._id}>

+ 15 - 5
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';
@@ -13,8 +15,9 @@ import {
   apiv3Get, apiv3Put, apiv3Delete, apiv3Post,
 } from '~/client/util/apiv3-client';
 import { toastSuccess, toastError } from '~/client/util/toastr';
-import { IExternalUserGroupHasId } from '~/features/external-user-group/interfaces/external-user-group';
-import { SearchTypes, SearchType } from '~/interfaces/user-group';
+import type { IExternalUserGroupHasId } from '~/features/external-user-group/interfaces/external-user-group';
+import type { PageActionOnGroupDelete, SearchType } from '~/interfaces/user-group';
+import { SearchTypes } from '~/interfaces/user-group';
 import Xss from '~/services/xss';
 import { useIsAclEnabled } from '~/stores/context';
 import { useUpdateUserGroupConfirmModal } from '~/stores/modal';
@@ -84,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);
 
@@ -296,12 +303,15 @@ const UserGroupDetailPage = (props: Props): JSX.Element => {
     setDeleteModalShown(false);
   }, [setSelectedUserGroup, setDeleteModalShown]);
 
-  const deleteChildUserGroupById = useCallback(async(deleteGroupId: string, actionName: string, 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
@@ -448,7 +458,7 @@ const UserGroupDetailPage = (props: Props): JSX.Element => {
       />
 
       <UserGroupDeleteModal
-        userGroups={childUserGroups}
+        userGroups={childUserGroupsForDeleteModal}
         deleteUserGroup={selectedUserGroup}
         onDelete={deleteChildUserGroupById}
         isShow={isDeleteModalShown}

+ 8 - 4
apps/app/src/components/Admin/Users/SortIcons.tsx

@@ -13,15 +13,19 @@ export const SortIcons = (props: SortIconsProps): JSX.Element => {
   return (
     <div className="d-flex flex-column text-center">
       <a
-        className={`fa ${isSelected && isAsc ? 'fa-chevron-up' : 'fa-angle-up'}`}
+        className={`${isSelected && isAsc ? 'text-primary' : 'text-muted'}`}
         aria-hidden="true"
         onClick={() => onClick('asc')}
-      />
+      >
+        <span className="material-symbols-outlined">expand_less</span>
+      </a>
       <a
-        className={`fa ${isSelected && !isAsc ? 'fa-chevron-down' : 'fa-angle-down'}`}
+        className={`${isSelected && !isAsc ? 'text-primary' : 'text-muted'}`}
         aria-hidden="true"
         onClick={() => onClick('desc')}
-      />
+      >
+        <span className="material-symbols-outlined">expand_more</span>
+      </a>
     </div>
   );
 };

+ 2 - 2
apps/app/src/components/Admin/Users/UserMenu.module.scss

@@ -1,5 +1,5 @@
 .grw-usermenu-notification-icon :global {
   position: absolute;
-  top: -4px;
-  left: 30px;
+  top: -6px;
+  left: 3px;
 }

+ 5 - 2
apps/app/src/components/Admin/Users/UserMenu.tsx

@@ -95,10 +95,13 @@ const UserMenu = (props: UserMenuProps) => {
   return (
     <UncontrolledDropdown id="userMenu" size="sm">
       <DropdownToggle caret color="secondary" outline>
-        {/* TODO:fontsize: 20px */}
         <span className="material-symbols-outlined fs-5">settings</span>
         {(user.status === USER_STATUS.INVITED && !isInvitationEmailSended)
-        && <i className={`fa fa-circle text-danger grw-usermenu-notification-icon ${styles['grw-usermenu-notification-icon']}`} />}
+        && (
+          <span className={`material-symbols-outlined fill fs-6 text-danger grw-usermenu-notification-icon ${styles['grw-usermenu-notification-icon']}`}>
+            circle
+          </span>
+        )}
       </DropdownToggle>
       <DropdownMenu strategy="fixed">
         {renderEditMenu()}

+ 1 - 1
apps/app/src/components/Bookmarks/BookmarkFolderItemControl.tsx

@@ -35,7 +35,7 @@ export const BookmarkFolderItemControl: React.FC<{
             onClick={onClickMoveToRoot}
             className="grw-page-control-dropdown-item"
           >
-            <i className="fa fa-fw fa-bookmark-o grw-page-control-dropdown-icon"></i>
+            <span className="material-symbols-outlined grw-page-control-dropdown-icon">bookmark</span>
             {t('bookmark_folder.move_to_root')}
           </DropdownItem>
         )}

+ 1 - 1
apps/app/src/components/Bookmarks/BookmarkFolderMenu.tsx

@@ -118,7 +118,7 @@ export const BookmarkFolderMenu = (props: BookmarkFolderMenuProps): JSX.Element
           onClick={onUnbookmarkHandler}
           className="grw-bookmark-folder-menu-item text-danger"
         >
-          <i className="fa fa-bookmark"></i>{' '}
+          <span className="material-symbols-outlined">bookmark</span>{' '}
           <span className="mx-2">
             {t('bookmark_folder.cancel_bookmark')}
           </span>

+ 1 - 1
apps/app/src/components/Bookmarks/BookmarkMoveToRootBtn.tsx

@@ -15,7 +15,7 @@ export const BookmarkMoveToRootBtn: React.FC<{
       className="grw-page-control-dropdown-item"
       data-testid="add-remove-bookmark-btn"
     >
-      <i className="fa fa-fw fa-bookmark-o grw-page-control-dropdown-icon"></i>
+      <span className="material-symbols-outlined grw-page-control-dropdown-icon">bookmark</span>
       {t('bookmark_folder.move_to_root')}
     </DropdownItem>
   );

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

@@ -1,4 +1,5 @@
-import React, { FC, useState, useCallback } from 'react';
+import type { FC } from 'react';
+import React, { useState, useCallback } from 'react';
 
 import { CopyToClipboard } from 'react-copy-to-clipboard';
 import { useTranslation } from 'react-i18next';
@@ -25,7 +26,7 @@ const CustomCopyToClipBoard: FC<Props> = (props: Props) => {
     <>
       <CopyToClipboard text={props.textToBeCopied || ''} onCopy={showToolTip}>
         <div className="btn input-group-text" id="tooltipTarget">
-          <i className="fa fa-clipboard mx-1" aria-hidden="true"></i>
+          <span className="material-symbols-outlined mx-1" aria-hidden="true">content_paste</span>
         </div>
       </CopyToClipboard>
       <Tooltip target="tooltipTarget" fade={false} isOpen={tooltipOpen}>

+ 1 - 1
apps/app/src/components/Common/Dropdown/PageItemControl.tsx

@@ -171,7 +171,7 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
             className="grw-page-control-dropdown-item"
             data-testid="add-remove-bookmark-btn"
           >
-            <i className="fa fa-fw fa-bookmark-o grw-page-control-dropdown-icon"></i>
+            <span className="material-symbols-outlined grw-page-control-dropdown-icon">bookmark</span>
             { pageInfo.isBookmarked ? t('remove_bookmark') : t('add_bookmark') }
           </DropdownItem>
         ) }

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

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

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

@@ -66,7 +66,7 @@ const AssociateModal = (props: Props): JSX.Element => {
               className={activeTab === 1 ? 'active' : ''}
               onClick={() => setActiveTab(1)}
             >
-              <i className="fa fa-sitemap"></i> LDAP
+              <span className="material-symbols-outlined fs-5">network_node</span> LDAP
             </NavLink>
             <NavLink
               className={activeTab === 2 ? 'active' : ''}
@@ -113,7 +113,7 @@ const AssociateModal = (props: Props): JSX.Element => {
       </ModalBody>
       <ModalFooter className="border-top-0">
         <button type="button" className="btn btn-primary mt-3" onClick={clickAddLdapAccountHandler}>
-          <i className="fa fa-plus-circle" aria-hidden="true"></i>
+          <span className="material-symbols-outlined" aria-hidden="true">add_circle</span>
           {t('add')}
         </button>
       </ModalFooter>

+ 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

@@ -43,7 +43,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">
@@ -51,9 +51,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
@@ -68,17 +68,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 - 3
apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -322,14 +322,14 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
         )}
 
         { isGuestUser && (
-          <>
-            <Link href="/login#register" className="btn" prefetch={false}>
+          <div className="mt-2">
+            <Link href="/login#register" className="btn me-2" prefetch={false}>
               <span className="material-symbols-outlined me-1">person_add</span>{t('Sign up')}
             </Link>
             <Link href="/login#login" className="btn btn-primary" prefetch={false}>
               <span className="material-symbols-outlined me-1">login</span>{t('Sign in')}
             </Link>
-          </>
+          </div>
         ) }
       </div>
 

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

+ 4 - 2
apps/app/src/components/PageControls/LikeButtons.tsx

@@ -1,4 +1,5 @@
-import React, { FC, useState, useCallback } from 'react';
+import type { FC } from 'react';
+import React, { useState, useCallback } from 'react';
 
 import type { IUser } from '@growi/core';
 import { useTranslation } from 'next-i18next';
@@ -8,6 +9,7 @@ import { UncontrolledTooltip, Popover, PopoverBody } from 'reactstrap';
 import UserPictureList from '../Common/UserPictureList';
 
 import styles from './LikeButtons.module.scss';
+import popoverStyles from './user-list-popover.module.scss';
 
 type LikeButtonsProps = {
 
@@ -65,7 +67,7 @@ const LikeButtons: FC<LikeButtonsProps> = (props: LikeButtonsProps) => {
         {sumOfLikers}
       </button>
       <Popover placement="bottom" isOpen={isPopoverOpen} target="po-total-likes" toggle={togglePopover} trigger="legacy">
-        <PopoverBody className="user-list-popover">
+        <PopoverBody className={`user-list-popover ${popoverStyles['user-list-popover']}`}>
           <div className="px-2 text-end user-list-content text-truncate text-muted">
             {props.likers?.length ? <UserPictureList users={props.likers} /> : t('No users have liked this yet.')}
           </div>

+ 2 - 1
apps/app/src/components/PageControls/SeenUserInfo.tsx

@@ -1,4 +1,5 @@
-import React, { FC, useState } from 'react';
+import type { FC } from 'react';
+import React, { useState } from 'react';
 
 import type { IUser } from '@growi/core';
 import { FootstampIcon } from '@growi/ui/dist/components';

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

+ 3 - 10
apps/app/src/components/PageControls/user-list-popover.module.scss

@@ -1,12 +1,5 @@
-.user-list-popover :global {
-  --bs-popover-max-width: 200px;
-  --bs-popover-body-padding-x: .5rem;
-  --bs-popover-body-padding-y: .5rem;
+@use '@growi/ui/scss/molecules/user-list-popover';
 
-  .user-list-content {
-    direction: rtl;
-  }
-  .cls-1 {
-    isolation: isolate;
-  }
+.user-list-popover :global {
+  @extend %user-list-popover
 }

+ 59 - 90
apps/app/src/components/PageCreateModal.jsx → apps/app/src/components/PageCreateModal.tsx

@@ -2,36 +2,42 @@ 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';
-import { useRouter } from 'next/router';
 import {
   Modal, ModalHeader, ModalBody, UncontrolledButtonDropdown, DropdownToggle, DropdownMenu, DropdownItem,
 } from 'reactstrap';
 import { debounce } from 'throttle-debounce';
 
-import { toastError } from '~/client/util/toastr';
+import { useCreateTemplatePage } from '~/client/services/create-page';
+import { useCreatePageAndTransit } from '~/client/services/create-page/use-create-page-and-transit';
+import { useToastrOnError } from '~/client/services/use-toastr-on-error';
 import { useCurrentUser, useIsSearchServiceReachable } from '~/stores/context';
 import { usePageCreateModal } from '~/stores/modal';
-import { EditorMode, useEditorMode } from '~/stores/ui';
+
 
 import PagePathAutoComplete from './PagePathAutoComplete';
 
 import styles from './PageCreateModal.module.scss';
 
 const {
-  isCreatablePage, generateEditorPath, isUsersHomepage,
+  isCreatablePage, isUsersHomepage,
 } = pagePathUtils;
 
-const PageCreateModal = () => {
+const PageCreateModal: React.FC = () => {
   const { t } = useTranslation();
-  const router = useRouter();
 
   const { data: currentUser } = useCurrentUser();
 
   const { data: pageCreateModalData, close: closeCreateModal } = usePageCreateModal();
-  const { isOpened, path } = pageCreateModalData;
+
+  const path = pageCreateModalData?.path;
+  const isOpened = pageCreateModalData?.isOpened ?? false;
+
+  const { createAndTransit } = useCreatePageAndTransit();
+  const { createTemplate } = useCreateTemplatePage();
 
   const { data: isReachable } = useIsSearchServiceReachable();
   const pathname = path || '';
@@ -39,26 +45,13 @@ const PageCreateModal = () => {
   const isCreatable = isCreatablePage(pathname) || isUsersHomepage(pathname);
   const pageNameInputInitialValue = isCreatable ? pathUtils.addTrailingSlash(pathname) : '/';
   const now = format(new Date(), 'yyyy/MM/dd');
+  const todaysParentPath = [userHomepagePath, t('create_page_dropdown.todays.memo', { ns: 'commons' }), now].join('/');
 
-  const { mutate: mutateEditorMode } = useEditorMode();
-
-  const [todayInput1, setTodayInput1] = useState(t('Memo'));
-  const [todayInput2, setTodayInput2] = useState('');
+  const [todayInput, setTodayInput] = useState('');
   const [pageNameInput, setPageNameInput] = useState(pageNameInputInitialValue);
   const [template, setTemplate] = useState(null);
   const [isMatchedWithUserHomepagePath, setIsMatchedWithUserHomepagePath] = useState(false);
 
-  // ensure pageNameInput is synced with selectedPagePath || currentPagePath
-  useEffect(() => {
-    if (isOpened) {
-      setPageNameInput(isCreatable ? pathUtils.addTrailingSlash(pathname) : '/');
-    }
-  }, [isOpened, pathname, isCreatable]);
-
-  useEffect(() => {
-    setTodayInput1(t('Memo'));
-  }, [t]);
-
   const checkIsUsersHomepageDebounce = useMemo(() => {
     const checkIsUsersHomepage = () => {
       setIsMatchedWithUserHomepagePath(isUsersHomepage(pageNameInput));
@@ -69,10 +62,11 @@ const PageCreateModal = () => {
 
   useEffect(() => {
     if (isOpened) {
-      checkIsUsersHomepageDebounce(pageNameInput);
+      checkIsUsersHomepageDebounce();
     }
   }, [isOpened, checkIsUsersHomepageDebounce, pageNameInput]);
 
+
   function transitBySubmitEvent(e, transitHandler) {
     // prevent page transition by submit
     e.preventDefault();
@@ -80,19 +74,11 @@ const PageCreateModal = () => {
   }
 
   /**
-   * change todayInput1
-   * @param {string} value
-   */
-  function onChangeTodayInput1Handler(value) {
-    setTodayInput1(value);
-  }
-
-  /**
-   * change todayInput2
+   * change todayInput
    * @param {string} value
    */
-  function onChangeTodayInput2Handler(value) {
-    setTodayInput2(value);
+  function onChangeTodayInputHandler(value) {
+    setTodayInput(value);
   }
 
   /**
@@ -103,53 +89,45 @@ const PageCreateModal = () => {
     setTemplate(value);
   }
 
-  /**
-   * join path, check if creatable, then redirect
-   * @param {string} paths
-   */
-  const redirectToEditor = useCallback(async(...paths) => {
-    try {
-      const editorPath = generateEditorPath(...paths);
-      await router.push(editorPath);
-      mutateEditorMode(EditorMode.Editor);
-
-      // close modal
-      closeCreateModal();
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }, [closeCreateModal, mutateEditorMode, router]);
-
   /**
    * access today page
    */
-  function createTodayPage() {
-    let tmpTodayInput1 = todayInput1;
-    if (tmpTodayInput1 === '') {
-      tmpTodayInput1 = t('Memo');
-    }
-    redirectToEditor(userHomepagePath, tmpTodayInput1, now, todayInput2);
-  }
+  const createTodayPage = useCallback(async() => {
+    const joinedPath = [todaysParentPath, todayInput].join('/');
+    return createAndTransit(
+      { path: joinedPath, wip: true, origin: Origin.View },
+      { shouldCheckPageExists: true, onTerminated: closeCreateModal },
+    );
+  }, [closeCreateModal, createAndTransit, todayInput, todaysParentPath]);
 
   /**
    * access input page
    */
-  function createInputPage() {
-    redirectToEditor(pageNameInput);
-  }
-
-  const ppacSubmitHandler = useCallback((input) => {
-    redirectToEditor(input);
-  }, [redirectToEditor]);
+  const createInputPage = useCallback(async() => {
+    return createAndTransit(
+      {
+        path: pageNameInput,
+        wip: true,
+        origin: Origin.View,
+      },
+      { shouldCheckPageExists: true, onTerminated: closeCreateModal },
+    );
+  }, [closeCreateModal, createAndTransit, pageNameInput]);
 
   /**
    * access template page
    */
-  function createTemplatePage(e) {
-    const pageName = (template === 'children') ? '_template' : '__template';
-    redirectToEditor(pathname, pageName);
-  }
+  const createTemplatePage = useCallback(async() => {
+
+    const label = (template === 'children') ? '_template' : '__template';
+
+    await createTemplate?.(label);
+    closeCreateModal();
+  }, [closeCreateModal, createTemplate, template]);
+
+  const createTodaysMemoWithToastr = useToastrOnError(createTodayPage);
+  const createInputPageWithToastr = useToastrOnError(createInputPage);
+  const createTemplateWithToastr = useToastrOnError(createTemplatePage);
 
   function renderCreateTodayForm() {
     if (!isOpened) {
@@ -158,31 +136,22 @@ const PageCreateModal = () => {
     return (
       <div className="row">
         <fieldset className="col-12 mb-4">
-          <h3 className="grw-modal-head pb-2">{t("Create today's")}</h3>
+          <h3 className="grw-modal-head pb-2">{t('create_page_dropdown.todays.desc', { ns: 'commons' })}</h3>
 
           <div className="d-sm-flex align-items-center justify-items-between">
 
             <div className="d-flex align-items-center flex-fill flex-wrap flex-lg-nowrap">
-              <div className="d-flex align-items-center">
-                <span>{userHomepagePath}/</span>
-                <form onSubmit={e => transitBySubmitEvent(e, createTodayPage)}>
-                  <input
-                    type="text"
-                    className="page-today-input1 form-control text-center mx-2"
-                    value={todayInput1}
-                    onChange={e => onChangeTodayInput1Handler(e.target.value)}
-                  />
-                </form>
-                <span className="page-today-suffix">/{now}/</span>
+              <div className="d-flex align-items-center text-nowrap">
+                <span>{todaysParentPath}/</span>
               </div>
-              <form className="mt-1 mt-lg-0 ms-lg-2 w-100" onSubmit={e => transitBySubmitEvent(e, createTodayPage)}>
+              <form className="mt-1 mt-lg-0 ms-lg-2 w-100" onSubmit={(e) => { transitBySubmitEvent(e, createTodaysMemoWithToastr) }}>
                 <input
                   type="text"
                   className="page-today-input2 form-control w-100"
                   id="page-today-input2"
                   placeholder={t('Input page name (optional)')}
-                  value={todayInput2}
-                  onChange={e => onChangeTodayInput2Handler(e.target.value)}
+                  value={todayInput}
+                  onChange={e => onChangeTodayInputHandler(e.target.value)}
                 />
               </form>
             </div>
@@ -192,7 +161,7 @@ const PageCreateModal = () => {
                 type="button"
                 data-testid="btn-create-memo"
                 className="grw-btn-create-page btn btn-outline-primary rounded-pill text-nowrap ms-3"
-                onClick={createTodayPage}
+                onClick={createTodaysMemoWithToastr}
               >
                 <span className="material-symbols-outlined">description</span>{t('Create')}
               </button>
@@ -221,13 +190,13 @@ const PageCreateModal = () => {
                   <PagePathAutoComplete
                     initializedPath={pageNameInputInitialValue}
                     addTrailingSlash
-                    onSubmit={ppacSubmitHandler}
+                    onSubmit={createInputPageWithToastr}
                     onInputChange={value => setPageNameInput(value)}
                     autoFocus
                   />
                 )
                 : (
-                  <form onSubmit={e => transitBySubmitEvent(e, createInputPage)}>
+                  <form onSubmit={(e) => { transitBySubmitEvent(e, createInputPageWithToastr) }}>
                     <input
                       type="text"
                       value={pageNameInput}
@@ -245,7 +214,7 @@ const PageCreateModal = () => {
                 type="button"
                 data-testid="btn-create-page-under-below"
                 className="grw-btn-create-page btn btn-outline-primary rounded-pill text-nowrap ms-3"
-                onClick={createInputPage}
+                onClick={createInputPageWithToastr}
                 disabled={isMatchedWithUserHomepagePath}
               >
                 <span className="material-symbols-outlined">description</span>{t('Create')}
@@ -300,7 +269,7 @@ const PageCreateModal = () => {
                 data-testid="grw-btn-edit-page"
                 type="button"
                 className="grw-btn-create-page btn btn-outline-primary rounded-pill text-nowrap ms-3"
-                onClick={createTemplatePage}
+                onClick={createTemplateWithToastr}
                 disabled={template == null}
               >
                 <span className="material-symbols-outlined">description</span>{t('Edit')}

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

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

@@ -0,0 +1,5 @@
+@use '@growi/ui/scss/molecules/user-list-popover';
+
+.user-list-popover :global {
+  @extend %user-list-popover;
+}

+ 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 - 1
apps/app/src/components/PageEditor/EditorNavbarBottom.tsx

@@ -91,7 +91,7 @@ const EditorNavbarBottom = (): JSX.Element => {
             >
               <div className="grw-slack-logo">
                 <SlackLogo />
-                <span className="grw-btn-slack-triangle fa fa-caret-up ms-2"></span>
+                <span className="grw-btn-slack-triangle material-symbols-outlined ms-2">arrow_drop_up</span>
               </div>
             </Button>
           ) : (

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