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

Merge branch 'dev/7.0.x' into imprv/136128-hide-title-of-untitled-page

kosei-n 2 лет назад
Родитель
Сommit
570ccb4f41
100 измененных файлов с 838 добавлено и 1014 удалено
  1. 4 3
      apps/app/package.json
  2. BIN
      apps/app/public/images/icons/sublime.png
  3. BIN
      apps/app/public/images/icons/vscode.png
  4. 1 1
      apps/app/public/static/locales/en_US/admin.json
  5. 16 3
      apps/app/public/static/locales/en_US/translation.json
  6. 1 1
      apps/app/public/static/locales/ja_JP/admin.json
  7. 16 3
      apps/app/public/static/locales/ja_JP/translation.json
  8. 1 1
      apps/app/public/static/locales/zh_CN/admin.json
  9. 16 3
      apps/app/public/static/locales/zh_CN/translation.json
  10. 2 0
      apps/app/src/client/services/create-page/index.ts
  11. 110 0
      apps/app/src/client/services/create-page/use-create-page-and-transit.tsx
  12. 38 0
      apps/app/src/client/services/create-page/use-create-template-page.ts
  13. 30 79
      apps/app/src/client/services/page-operation.ts
  14. 11 28
      apps/app/src/client/services/side-effects/drawio-modal-launcher-for-view.ts
  15. 14 29
      apps/app/src/client/services/side-effects/handsontable-modal-launcher-for-view.ts
  16. 0 123
      apps/app/src/client/services/use-create-page-and-transit.tsx
  17. 0 55
      apps/app/src/client/services/use-on-template-button-clicked.ts
  18. 2 2
      apps/app/src/client/util/bookmark-utils.ts
  19. 9 10
      apps/app/src/components/Admin/AuditLog/SearchUsernameTypeahead.tsx
  20. 1 1
      apps/app/src/components/Admin/Common/AdminNavigation.tsx
  21. 4 5
      apps/app/src/components/Admin/Security/GitHubSecuritySettingContents.jsx
  22. 4 4
      apps/app/src/components/Admin/Security/GoogleSecuritySettingContents.jsx
  23. 6 6
      apps/app/src/components/Admin/Security/OidcSecuritySettingContents.jsx
  24. 8 4
      apps/app/src/components/Admin/Security/SamlSecuritySettingContents.jsx
  25. 1 1
      apps/app/src/components/Admin/Security/SecurityManagementContents.jsx
  26. 6 6
      apps/app/src/components/Admin/Security/SecuritySetting.jsx
  27. 9 3
      apps/app/src/components/Admin/SlackIntegration/Bridge.jsx
  28. 1 1
      apps/app/src/components/Admin/SlackIntegration/CustomBotWithoutProxySettingsAccordion.jsx
  29. 1 1
      apps/app/src/components/Admin/SlackIntegration/SlackIntegration.jsx
  30. 1 1
      apps/app/src/components/Admin/SlackIntegration/WithProxyAccordions.jsx
  31. 2 3
      apps/app/src/components/Admin/UserGroup/UserGroupModal.tsx
  32. 5 6
      apps/app/src/components/Admin/UserGroup/UserGroupTable.tsx
  33. 4 3
      apps/app/src/components/Admin/UserGroupDetail/UpdateParentConfirmModal.tsx
  34. 0 169
      apps/app/src/components/Admin/UserGroupDetail/UserGroupUserFormByInput.jsx
  35. 122 0
      apps/app/src/components/Admin/UserGroupDetail/UserGroupUserFormByInput.tsx
  36. 6 5
      apps/app/src/components/Admin/UserGroupDetail/UserGroupUserModal.tsx
  37. 3 3
      apps/app/src/components/Admin/UserGroupDetail/UserGroupUserTable.tsx
  38. 1 1
      apps/app/src/components/Admin/UserManagement.module.scss
  39. 4 3
      apps/app/src/components/Admin/UserManagement.tsx
  40. 2 2
      apps/app/src/components/Admin/Users/ExternalAccountTable.tsx
  41. 1 1
      apps/app/src/components/Admin/Users/GrantAdminButton.tsx
  42. 1 1
      apps/app/src/components/Admin/Users/GrantReadOnlyButton.tsx
  43. 2 2
      apps/app/src/components/Admin/Users/RevokeAdminButton.tsx
  44. 2 2
      apps/app/src/components/Admin/Users/RevokeAdminMenuItem.tsx
  45. 1 1
      apps/app/src/components/Admin/Users/RevokeReadOnlyMenuItem.tsx
  46. 1 1
      apps/app/src/components/Admin/Users/SendInvitationEmailButton.jsx
  47. 1 1
      apps/app/src/components/Admin/Users/StatusActivateButton.jsx
  48. 2 2
      apps/app/src/components/Admin/Users/StatusSuspendMenuItem.tsx
  49. 3 2
      apps/app/src/components/Admin/Users/UserMenu.tsx
  50. 3 3
      apps/app/src/components/Bookmarks/BookmarkFolderItemControl.tsx
  51. 0 4
      apps/app/src/components/Bookmarks/BookmarkFolderTree.module.scss
  52. 4 3
      apps/app/src/components/Bookmarks/BookmarkItem.tsx
  53. 25 18
      apps/app/src/components/Common/ClosableTextInput.tsx
  54. 2 1
      apps/app/src/components/Common/CountBadge.tsx
  55. 6 6
      apps/app/src/components/Common/Dropdown/PageItemControl.tsx
  56. 3 2
      apps/app/src/components/Common/PagePathHierarchicalLink/PagePathHierarchicalLink.tsx
  57. 5 0
      apps/app/src/components/Common/PagePathNav/PagePathNav.module.scss
  58. 19 6
      apps/app/src/components/Common/PagePathNav/PagePathNav.tsx
  59. 0 6
      apps/app/src/components/ContentLinkButtons.module.scss
  60. 13 18
      apps/app/src/components/ContentLinkButtons.tsx
  61. 13 8
      apps/app/src/components/CreateTemplateModal.tsx
  62. 5 12
      apps/app/src/components/CustomNavigation/CustomNav.module.scss
  63. 3 3
      apps/app/src/components/CustomNavigation/CustomNav.tsx
  64. 3 5
      apps/app/src/components/DescendantsPageListModal.tsx
  65. 4 2
      apps/app/src/components/ExpandOrContractButton.tsx
  66. 0 28
      apps/app/src/components/Icons/AttachmentIcon.jsx
  67. 0 22
      apps/app/src/components/Icons/HistoryIcon.jsx
  68. 0 17
      apps/app/src/components/Icons/PageListIcon.jsx
  69. 0 22
      apps/app/src/components/Icons/PresentationIcon.jsx
  70. 0 35
      apps/app/src/components/Icons/ShareLinkIcon.jsx
  71. 0 19
      apps/app/src/components/Icons/TimeLineIcon.jsx
  72. 0 5
      apps/app/src/components/ItemsTree/ItemsTree.module.scss
  73. 14 8
      apps/app/src/components/ItemsTree/ItemsTree.tsx
  74. 13 13
      apps/app/src/components/Me/BasicInfoSettings.tsx
  75. 1 1
      apps/app/src/components/Me/ExternalAccountLinkedMe.jsx
  76. 6 6
      apps/app/src/components/Me/PersonalSettings.jsx
  77. 19 19
      apps/app/src/components/Me/ProfileImageSettings.tsx
  78. 2 2
      apps/app/src/components/Me/UserSettings.tsx
  79. 20 18
      apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  80. 26 23
      apps/app/src/components/Navbar/PageEditorModeManager.tsx
  81. 2 4
      apps/app/src/components/NotFoundPage.tsx
  82. 3 3
      apps/app/src/components/Page/PageView.tsx
  83. 2 2
      apps/app/src/components/Page/RevisionLoader.tsx
  84. 3 2
      apps/app/src/components/Page/ShareLinkAlert.tsx
  85. 3 6
      apps/app/src/components/PageAccessoriesModal/PageAccessoriesModal.tsx
  86. 1 1
      apps/app/src/components/PageAccessoriesModal/PageAttachment.tsx
  87. 3 3
      apps/app/src/components/PageAlert/FixPageGrantAlert.tsx
  88. 1 1
      apps/app/src/components/PageAlert/OldRevisionAlert.tsx
  89. 2 0
      apps/app/src/components/PageAlert/PageAlerts.tsx
  90. 3 3
      apps/app/src/components/PageAlert/PageGrantAlert.tsx
  91. 1 1
      apps/app/src/components/PageAlert/PageStaleAlert.tsx
  92. 1 1
      apps/app/src/components/PageAlert/TrashPageAlert.tsx
  93. 53 0
      apps/app/src/components/PageAlert/WipPageAlert.tsx
  94. 2 2
      apps/app/src/components/PageAttachment/DeleteAttachmentModal.tsx
  95. 2 3
      apps/app/src/components/PageComment/Comment.tsx
  96. 48 47
      apps/app/src/components/PageComment/CommentEditor.tsx
  97. 2 2
      apps/app/src/components/PageComment/ReplyComments.tsx
  98. 4 0
      apps/app/src/components/PageContentFooter.tsx
  99. 9 8
      apps/app/src/components/PageControls/PageControls.tsx
  100. 8 8
      apps/app/src/components/PageDuplicateModal.tsx

+ 4 - 3
apps/app/package.json

@@ -69,8 +69,8 @@
     "@elastic/elasticsearch8": "npm:@elastic/elasticsearch@^8.7.0",
     "@elastic/elasticsearch8": "npm:@elastic/elasticsearch@^8.7.0",
     "@godaddy/terminus": "^4.9.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/custom-icons": "link:../../packages/custom-icons",
     "@growi/core": "link:../../packages/core",
     "@growi/core": "link:../../packages/core",
+    "@growi/custom-icons": "link:../../packages/custom-icons",
     "@growi/pluginkit": "link:../../packages/pluginkit",
     "@growi/pluginkit": "link:../../packages/pluginkit",
     "@growi/preset-templates": "link:../../packages/preset-templates",
     "@growi/preset-templates": "link:../../packages/preset-templates",
     "@growi/preset-themes": "link:../../packages/preset-themes",
     "@growi/preset-themes": "link:../../packages/preset-themes",
@@ -163,7 +163,7 @@
     "qs": "^6.11.1",
     "qs": "^6.11.1",
     "rate-limiter-flexible": "^2.3.7",
     "rate-limiter-flexible": "^2.3.7",
     "react": "^18.2.0",
     "react": "^18.2.0",
-    "react-bootstrap-typeahead": "^5.2.2",
+    "react-bootstrap-typeahead": "^6.3.2",
     "react-card-flip": "^1.0.10",
     "react-card-flip": "^1.0.10",
     "react-datepicker": "^4.7.0",
     "react-datepicker": "^4.7.0",
     "react-disable": "^0.1.1",
     "react-disable": "^0.1.1",
@@ -235,8 +235,8 @@
     "@types/jest": "^29.5.2",
     "@types/jest": "^29.5.2",
     "@types/react-scroll": "^1.8.4",
     "@types/react-scroll": "^1.8.4",
     "@types/throttle-debounce": "^5.0.1",
     "@types/throttle-debounce": "^5.0.1",
-    "@types/url-join": "^4.0.2",
     "@types/unzip-stream": "^0.3.4",
     "@types/unzip-stream": "^0.3.4",
+    "@types/url-join": "^4.0.2",
     "@vitejs/plugin-react": "^4.2.1",
     "@vitejs/plugin-react": "^4.2.1",
     "@vitest/coverage-v8": "^0.34.6",
     "@vitest/coverage-v8": "^0.34.6",
     "autoprefixer": "^9.0.0",
     "autoprefixer": "^9.0.0",
@@ -270,6 +270,7 @@
     "react-copy-to-clipboard": "^5.0.1",
     "react-copy-to-clipboard": "^5.0.1",
     "react-dropzone": "^11.2.4",
     "react-dropzone": "^11.2.4",
     "react-hotkeys": "^2.0.0",
     "react-hotkeys": "^2.0.0",
+    "react-input-autosize": "^3.0.0",
     "rehype-rewrite": "^3.0.6",
     "rehype-rewrite": "^3.0.6",
     "replacestream": "^4.0.3",
     "replacestream": "^4.0.3",
     "sass": "^1.53.0",
     "sass": "^1.53.0",

BIN
apps/app/public/images/icons/sublime.png


BIN
apps/app/public/images/icons/vscode.png


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

@@ -746,7 +746,7 @@
       "description1":"Temporarily issue new users by email addresses.",
       "description1":"Temporarily issue new users by email addresses.",
       "description2":"A temporary password will be generated for the first login.",
       "description2":"A temporary password will be generated for the first login.",
       "invite_thru_email": "Send invitation email",
       "invite_thru_email": "Send invitation email",
-      "mail_setting_link":"<i class='icon-settings me-2'></i><a href='/admin/app'>Email settings</a>",
+      "mail_setting_link":"<span className='material-symbols-outlined me-2'>settings</span><a href='/admin/app'>Email settings</a>",
       "valid_email": "Valid email address is required",
       "valid_email": "Valid email address is required",
       "temporary_password": "The created user has a temporary password",
       "temporary_password": "The created user has a temporary password",
       "send_new_password": "Please send the new password to the user.",
       "send_new_password": "Please send the new password to the user.",

+ 16 - 3
apps/app/public/static/locales/en_US/translation.json

@@ -391,11 +391,11 @@
       "Recursively": "Recursively",
       "Recursively": "Recursively",
       "Duplicate without exist path": "Duplicate without exist path",
       "Duplicate without exist path": "Duplicate without exist path",
       "Same page already exists": "Same page already exists",
       "Same page already exists": "Same page already exists",
-      "Only duplicate user related resources": "Only duplicate user related resources"
+      "Only duplicate user related pages": "Only duplicate pages you can access"
     },
     },
     "help": {
     "help": {
       "recursive": "Duplicate children of under this path recursively",
       "recursive": "Duplicate children of under this path recursively",
-      "only_user_related_resources": "This will only duplicate pages that the user has permission to view. If the page permission is set to \"Only specific groups\", only user related groups will be set to the page duplicate."
+      "only_inherit_user_related_groups": "If the page privilege is set to \"Only inside the group\", groups you do not belong to will lose access to the duplicated page"
     }
     }
   },
   },
   "duplicated_pages": "{{fromPath}} has been duplicated",
   "duplicated_pages": "{{fromPath}} has been duplicated",
@@ -538,6 +538,7 @@
     "search_again" : "Search again",
     "search_again" : "Search again",
     "number_of_list_to_display" : "Display",
     "number_of_list_to_display" : "Display",
     "page_number_unit" : "pages",
     "page_number_unit" : "pages",
+    "hit_number_unit" : "hit",
     "sort_axis": {
     "sort_axis": {
       "relationScore": "Sort by relevance",
       "relationScore": "Sort by relevance",
       "createdAt": "Creation date",
       "createdAt": "Creation date",
@@ -640,7 +641,7 @@
     "Username or E-mail has invalid characters": "Username or E-mail has invalid characters.",
     "Username or E-mail has invalid characters": "Username or E-mail has invalid characters.",
     "Password minimum character should be more than 6 characters": "Password minimum character should be more than 6 characters.",
     "Password minimum character should be more than 6 characters": "Password minimum character should be more than 6 characters.",
     "user_not_found": "User not found.",
     "user_not_found": "User not found.",
-    "provider_duplicated_username_exception": "<p><strong><i class='icon-fw icon-ban'></i>DuplicatedUsernameException occured</strong></p><p class='mb-0'> Your {{ failedProviderForDuplicatedUsernameException }} authentication was succeeded, but a new user could not be created. See the issue <a href='https://github.com/weseek/growi/issues/193'>#193</a>.</p>"
+    "provider_duplicated_username_exception": "<p><strong><span class='material-symbols-outlined me-1'>cancel</span>DuplicatedUsernameException occured</strong></p><p class='mb-0'> Your {{ failedProviderForDuplicatedUsernameException }} authentication was succeeded, but a new user could not be created. See the issue <a href='https://github.com/weseek/growi/issues/193'>#193</a>.</p>"
   },
   },
   "grid_edit":{
   "grid_edit":{
     "create_bootstrap_4_grid":"Create Bootstrap 4 Grid",
     "create_bootstrap_4_grid":"Create Bootstrap 4 Grid",
@@ -825,5 +826,17 @@
   },
   },
   "page_select_modal": {
   "page_select_modal": {
     "select_page_location": "Select page location"
     "select_page_location": "Select page location"
+  },
+  "wip_page": {
+    "save_as_wip": "Save as WIP (Currently drafting)",
+    "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",
+    "publish_page": "Publish page",
+    "success_publish_page": "Page has been published",
+    "fail_publish_page": "Failed to publish the Page"
+  },
+  "sidebar_header": {
+    "show_wip_page": "Show WIP"
   }
   }
 }
 }

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

@@ -756,7 +756,7 @@
       "description1": "メールアドレスを使用して新規ユーザーを仮発行します。",
       "description1": "メールアドレスを使用して新規ユーザーを仮発行します。",
       "description2": "初回のログイン時に使用する仮パスワードが生成されます。",
       "description2": "初回のログイン時に使用する仮パスワードが生成されます。",
       "invite_thru_email": "招待メールを送信する",
       "invite_thru_email": "招待メールを送信する",
-      "mail_setting_link": "<i class='icon-settings me-2'></i><a href='/admin/app'>メールの設定</a>",
+      "mail_setting_link": "<span className='material-symbols-outlined me-2'>settings</span><a href='/admin/app'>メールの設定</a>",
       "valid_email": "メールアドレスを入力してください。",
       "valid_email": "メールアドレスを入力してください。",
       "temporary_password": "作成したユーザーは仮パスワードが設定されています。",
       "temporary_password": "作成したユーザーは仮パスワードが設定されています。",
       "send_new_password": "新規発行したパスワードを、対象ユーザーへ連絡してください。",
       "send_new_password": "新規発行したパスワードを、対象ユーザーへ連絡してください。",

+ 16 - 3
apps/app/public/static/locales/ja_JP/translation.json

@@ -424,11 +424,11 @@
       "Recursively": "再帰的に複製",
       "Recursively": "再帰的に複製",
       "Duplicate without exist path": "存在するパス以外を複製する",
       "Duplicate without exist path": "存在するパス以外を複製する",
       "Same page already exists": "同じページがすでに存在します",
       "Same page already exists": "同じページがすでに存在します",
-      "Only duplicate user related resources": "ユーザに関連のあるリソースのみを複製する"
+      "Only duplicate user related pages": "自分が閲覧可能なページのみを複製する"
     },
     },
     "help": {
     "help": {
       "recursive": "配下のページも複製します",
       "recursive": "配下のページも複製します",
-      "only_user_related_resources": "ユーザが閲覧可能なページのみを複製します。また、閲覧権限が「特定グループのみ」で設定されている場合、複製後のページにはユーザが所属するグループのみを閲覧可能なグループとして設定します。"
+      "only_inherit_user_related_groups": "閲覧権限が「特定グループのみ」で設定されている場合、複製されたページを閲覧可能なグループ一覧から、自分が所属していないものは取り除かれます"
     }
     }
   },
   },
   "duplicated_pages": "{{fromPath}} を複製しました",
   "duplicated_pages": "{{fromPath}} を複製しました",
@@ -571,6 +571,7 @@
     "search_again" : "再検索",
     "search_again" : "再検索",
     "number_of_list_to_display" : "表示件数",
     "number_of_list_to_display" : "表示件数",
     "page_number_unit" : "件",
     "page_number_unit" : "件",
+    "hit_number_unit" : "件",
     "sort_axis": {
     "sort_axis": {
       "relationScore": "関連度順",
       "relationScore": "関連度順",
       "createdAt": "作成日時",
       "createdAt": "作成日時",
@@ -673,7 +674,7 @@
     "Username or E-mail has invalid characters": "ユーザー名または、メールアドレスに無効な文字があります",
     "Username or E-mail has invalid characters": "ユーザー名または、メールアドレスに無効な文字があります",
     "Password minimum character should be more than 6 characters": "パスワードの最小文字数は6文字以上です",
     "Password minimum character should be more than 6 characters": "パスワードの最小文字数は6文字以上です",
     "user_not_found": "ユーザーが見つかりません",
     "user_not_found": "ユーザーが見つかりません",
-    "provider_duplicated_username_exception": "<p><strong><i class='icon-fw icon-ban'></i>エラー: DuplicatedUsernameException</strong></p><p class='mb-0'> {{ failedProviderForDuplicatedUsernameException }} 認証は成功しましたが、新しいユーザーを作成できませんでした。詳しくは<a href='https://github.com/weseek/growi/issues/193'>こちら: #193</a>.</p>"
+    "provider_duplicated_username_exception": "<p><strong><span class='material-symbols-outlined me-1'>cancel</span>エラー: DuplicatedUsernameException</strong></p><p class='mb-0'> {{ failedProviderForDuplicatedUsernameException }} 認証は成功しましたが、新しいユーザーを作成できませんでした。詳しくは<a href='https://github.com/weseek/growi/issues/193'>こちら: #193</a>.</p>"
   },
   },
   "grid_edit":{
   "grid_edit":{
     "create_bootstrap_4_grid":"Bootstrap 4 グリッドを作成",
     "create_bootstrap_4_grid":"Bootstrap 4 グリッドを作成",
@@ -858,5 +859,17 @@
   },
   },
   "page_select_modal": {
   "page_select_modal": {
     "select_page_location": "ページの場所を選択"
     "select_page_location": "ページの場所を選択"
+  },
+  "wip_page": {
+    "save_as_wip": "WIP (執筆中) として保存",
+    "success_save_as_wip": "WIP ページとして保存しました",
+    "fail_save_as_wip": "WIP ページとして保存できませんでした",
+    "alert": "このページは作業途中です",
+    "publish_page": "WIP を解除",
+    "success_publish_page": "WIP を解除しました",
+    "fail_publish_page": "WIP を解除できませんでした"
+  },
+  "sidebar_header": {
+    "show_wip_page": "WIP を表示"
   }
   }
 }
 }

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

@@ -754,7 +754,7 @@
       "emails": "电子邮件",
       "emails": "电子邮件",
       "description1": "通过电子邮件地址临时发布新用户。",
       "description1": "通过电子邮件地址临时发布新用户。",
       "description2": "将为首次登录生成一个临时密码。",
       "description2": "将为首次登录生成一个临时密码。",
-      "mail_setting_link": "<i class='icon-settings me-2'></i><a href='/admin/app'>Email settings</a>",
+      "mail_setting_link": "<span className='material-symbols-outlined me-2'>settings</span><a href='/admin/app'>Email settings</a>",
       "valid_email": "需要有效的电子邮件地址",
       "valid_email": "需要有效的电子邮件地址",
       "invite_thru_email": "发送邀请电子邮件",
       "invite_thru_email": "发送邀请电子邮件",
       "temporary_password": "创建的用户具有临时密码",
       "temporary_password": "创建的用户具有临时密码",

+ 16 - 3
apps/app/public/static/locales/zh_CN/translation.json

@@ -381,11 +381,11 @@
       "Recursively": "Recursively",
       "Recursively": "Recursively",
       "Duplicate without exist path": "Duplicate without exist path",
       "Duplicate without exist path": "Duplicate without exist path",
       "Same page already exists": "Same page already exists",
       "Same page already exists": "Same page already exists",
-      "Only duplicate user related resources": "Only duplicate user related resources"
+      "Only duplicate user related pages": "Only duplicate pages you can access"
     },
     },
     "help": {
     "help": {
       "recursive": "Duplicate children of under this path recursively",
       "recursive": "Duplicate children of under this path recursively",
-      "only_user_related_resources": "This will only duplicate pages that the user has permission to view. If the page permission is set to \"Only specific groups\", only user related groups will be set to the page duplicate."
+      "only_inherit_user_related_groups": "If the page privilege is set to \"Only inside the group\", groups you do not belong to will lose access to the duplicated page"
     }
     }
   },
   },
   "duplicated_pages": "{{fromPath}} 已重复",
   "duplicated_pages": "{{fromPath}} 已重复",
@@ -541,6 +541,7 @@
     "search_again" : "再次搜索",
     "search_again" : "再次搜索",
     "number_of_list_to_display" : "显示器的数量",
     "number_of_list_to_display" : "显示器的数量",
     "page_number_unit" : "例",
     "page_number_unit" : "例",
+    "hit_number_unit" : "例",
     "sort_axis": {
     "sort_axis": {
       "relationScore": "按相关性排序",
       "relationScore": "按相关性排序",
       "createdAt": "按创建日期排序",
       "createdAt": "按创建日期排序",
@@ -643,7 +644,7 @@
     "Username or E-mail has invalid characters": "用户名或电子邮件有无效的字符",
     "Username or E-mail has invalid characters": "用户名或电子邮件有无效的字符",
     "Password minimum character should be more than 6 characters": "密码最小字符应超过6个字符",
     "Password minimum character should be more than 6 characters": "密码最小字符应超过6个字符",
     "user_not_found": "未找到用户",
     "user_not_found": "未找到用户",
-    "provider_duplicated_username_exception": "<p><strong><i class='icon-fw icon-ban'></i>发生了重复用户名异常</strong></p><p class='mb-0'> 你的 {{ failedProviderForDuplicatedUsernameException }} 认证成功了,但不能创建新的用户。参见问题<a href='https://github.com/weseek/growi/issues/193'>#193</a>.</p>"
+    "provider_duplicated_username_exception": "<p><strong><span class='material-symbols-outlined me-1'>cancel</span>发生了重复用户名异常</strong></p><p class='mb-0'> 你的 {{ failedProviderForDuplicatedUsernameException }} 认证成功了,但不能创建新的用户。参见问题<a href='https://github.com/weseek/growi/issues/193'>#193</a>.</p>"
 	},
 	},
   "grid_edit":{
   "grid_edit":{
     "create_bootstrap_4_grid":"创建Bootstrap 4网格",
     "create_bootstrap_4_grid":"创建Bootstrap 4网格",
@@ -828,5 +829,17 @@
   },
   },
   "page_select_modal": {
   "page_select_modal": {
     "select_page_location": "选择页面位置"
     "select_page_location": "选择页面位置"
+  },
+  "wip_page": {
+    "save_as_wip": "保存为 WIP(书面)",
+    "success_save_as_wip": "成功保存为 WIP 页面",
+    "fail_save_as_wip": "保存为 WIP 页失败",
+    "alert": "本页面正在制作中",
+    "publish_page": "发布 WIP",
+    "success_publish_page": "WIP 已停用",
+    "fail_publish_page": "无法停用 WIP"
+  },
+  "sidebar_header": {
+    "show_wip_page": "显示 WIP"
   }
   }
 }
 }

+ 2 - 0
apps/app/src/client/services/create-page/index.ts

@@ -0,0 +1,2 @@
+export * from './use-create-page-and-transit';
+export * from './use-create-template-page';

+ 110 - 0
apps/app/src/client/services/create-page/use-create-page-and-transit.tsx

@@ -0,0 +1,110 @@
+import { useCallback, useState } from 'react';
+
+import { useRouter } from 'next/router';
+
+import { createPage, exist } from '~/client/services/page-operation';
+import type { IApiv3PageCreateParams } from '~/interfaces/apiv3';
+import { useCurrentPagePath } from '~/stores/page';
+import { EditorMode, useEditorMode } from '~/stores/ui';
+import loggerFactory from '~/utils/logger';
+
+const logger = loggerFactory('growi:Navbar:GrowiContextualSubNavigation');
+
+/**
+ * Invoked when creation and transition has finished
+ */
+type OnCreated = () => void;
+/**
+ * Invoked when either creation or transition has aborted
+ */
+type OnAborted = () => void;
+/**
+ * Always invoked after processing is terminated
+ */
+type OnTerminated = () => void;
+
+type CreatePageAndTransitOpts = {
+  shouldCheckPageExists?: boolean,
+  onCreationStart?: OnCreated,
+  onCreated?: OnCreated,
+  onAborted?: OnAborted,
+  onTerminated?: OnTerminated,
+}
+
+type CreatePageAndTransit = (
+  params: IApiv3PageCreateParams,
+  opts?: CreatePageAndTransitOpts,
+) => Promise<void>;
+
+type UseCreatePageAndTransit = () => {
+  isCreating: boolean,
+  createAndTransit: CreatePageAndTransit,
+};
+
+export const useCreatePageAndTransit: UseCreatePageAndTransit = () => {
+
+  const router = useRouter();
+
+  const { data: currentPagePath } = useCurrentPagePath();
+  const { mutate: mutateEditorMode } = useEditorMode();
+
+  const [isCreating, setCreating] = useState(false);
+
+  const createAndTransit: CreatePageAndTransit = useCallback(async(params, opts = {}) => {
+    const {
+      shouldCheckPageExists,
+      onCreationStart, onCreated, onAborted, onTerminated,
+    } = opts;
+
+    // check the page existence
+    if (shouldCheckPageExists && params.path != null) {
+      const pagePath = params.path;
+
+      try {
+        const { isExist } = await exist(pagePath);
+
+        if (isExist) {
+          // routing
+          if (pagePath !== currentPagePath) {
+            await router.push(`${pagePath}#edit`);
+          }
+          mutateEditorMode(EditorMode.Editor);
+          onAborted?.();
+          return;
+        }
+      }
+      catch (err) {
+        throw err;
+      }
+      finally {
+        onTerminated?.();
+      }
+    }
+
+    // create and transit
+    try {
+      setCreating(true);
+      onCreationStart?.();
+
+      const response = await createPage(params);
+
+      await router.push(`/${response.page._id}#edit`);
+      mutateEditorMode(EditorMode.Editor);
+
+      onCreated?.();
+    }
+    catch (err) {
+      throw err;
+    }
+    finally {
+      onTerminated?.();
+      setCreating(false);
+    }
+
+  }, [currentPagePath, mutateEditorMode, router]);
+
+  return {
+    isCreating,
+    createAndTransit,
+  };
+};

+ 38 - 0
apps/app/src/client/services/create-page/use-create-template-page.ts

@@ -0,0 +1,38 @@
+import { useCallback } from 'react';
+
+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 = () => {
+  isCreatable: boolean,
+  isCreating: boolean,
+  createTemplate?: (label: LabelType) => Promise<void>,
+}
+
+export const useCreateTemplatePage: UseCreateTemplatePage = () => {
+
+  const { data: currentPagePath, isLoading: isLoadingPagePath } = useCurrentPagePath();
+
+  const { isCreating, createAndTransit } = useCreatePageAndTransit();
+  const isCreatable = currentPagePath != null && isCreatablePage(normalizePath(`${currentPagePath}/_template`));
+
+  const createTemplate = useCallback(async(label: LabelType) => {
+    if (isLoadingPagePath || !isCreatable) return;
+
+    return createAndTransit(
+      { path: normalizePath(`${currentPagePath}/${label}`), wip: false },
+      { shouldCheckPageExists: true },
+    );
+  }, [currentPagePath, isCreatable, isLoadingPagePath, createAndTransit]);
+
+  return {
+    isCreatable,
+    isCreating,
+    createTemplate: isCreatable ? createTemplate : undefined,
+  };
+};

+ 30 - 79
apps/app/src/client/services/page-operation.ts

@@ -1,20 +1,24 @@
 import { useCallback } from 'react';
 import { useCallback } from 'react';
 
 
-import { SubscriptionStatusType, type Nullable } from '@growi/core';
+import type { IPageHasId } from '@growi/core';
+import { SubscriptionStatusType } from '@growi/core';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 
 
-import { OptionsToSave } from '~/interfaces/page-operation';
-import { useEditingMarkdown, useIsEnabledUnsavedWarning, usePageTagsForEditors } from '~/stores/editor';
+import type {
+  IApiv3PageCreateParams, IApiv3PageCreateResponse, IApiv3PageUpdateParams, IApiv3PageUpdateResponse,
+} from '~/interfaces/apiv3';
+import { useEditingMarkdown, usePageTagsForEditors } from '~/stores/editor';
 import { useCurrentPageId, useSWRMUTxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
 import { useCurrentPageId, useSWRMUTxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
 import { useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
 import { useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-import { apiGet, apiPost } from '../util/apiv1-client';
-import { apiv3Post, apiv3Put } from '../util/apiv3-client';
+import { apiPost } from '../util/apiv1-client';
+import { apiv3Get, apiv3Post, apiv3Put } from '../util/apiv3-client';
 import { toastError } from '../util/toastr';
 import { toastError } from '../util/toastr';
 
 
 const logger = loggerFactory('growi:services:page-operation');
 const logger = loggerFactory('growi:services:page-operation');
 
 
+
 export const toggleSubscribe = async(pageId: string, currentStatus: SubscriptionStatusType | undefined): Promise<void> => {
 export const toggleSubscribe = async(pageId: string, currentStatus: SubscriptionStatusType | undefined): Promise<void> => {
   try {
   try {
     const newStatus = currentStatus === SubscriptionStatusType.SUBSCRIBE
     const newStatus = currentStatus === SubscriptionStatusType.SUBSCRIBE
@@ -87,71 +91,14 @@ export const resumeRenameOperation = async(pageId: string): Promise<void> => {
   await apiv3Post('/pages/resume-rename', { pageId });
   await apiv3Post('/pages/resume-rename', { pageId });
 };
 };
 
 
-// TODO: define return type
-export const createPage = async(pagePath: string, markdown: string, tmpParams: OptionsToSave) => {
-  // clone
-  const params = Object.assign(tmpParams, {
-    path: pagePath,
-    body: markdown,
-  });
-
-  const res = await apiv3Post('/pages/', params);
-  const { page, tags, revision } = res.data;
-
-  return { page, tags, revision };
-};
-
-// TODO: define return type
-const updatePage = async(pageId: string, revisionId: string, markdown: string, tmpParams: OptionsToSave) => {
-  // clone
-  const params = Object.assign(tmpParams, {
-    page_id: pageId,
-    revision_id: revisionId,
-    body: markdown,
-  });
-
-  const res: any = await apiPost('/pages.update', params);
-  if (!res.ok) {
-    throw new Error(res.error);
-  }
-  return res;
+export const createPage = async(params: IApiv3PageCreateParams): Promise<IApiv3PageCreateResponse> => {
+  const res = await apiv3Post<IApiv3PageCreateResponse>('/page', params);
+  return res.data;
 };
 };
 
 
-type PageInfo= {
-  path: string,
-  pageId: Nullable<string>,
-  revisionId: Nullable<string>,
-}
-
-type SaveOrUpdateFunction = (markdown: string, pageInfo: PageInfo, optionsToSave?: OptionsToSave) => any;
-
-// TODO: define return type
-export const useSaveOrUpdate = (): SaveOrUpdateFunction => {
-  /* eslint-disable react-hooks/rules-of-hooks */
-  const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
-  /* eslint-enable react-hooks/rules-of-hooks */
-
-  return useCallback(async(markdown: string, pageInfo: PageInfo, optionsToSave?: OptionsToSave) => {
-    const { path, pageId, revisionId } = pageInfo;
-
-    const options: OptionsToSave = Object.assign({}, optionsToSave);
-
-    let res;
-    if (pageId == null || revisionId == null) {
-      res = await createPage(path, markdown, options);
-    }
-    else {
-      if (revisionId == null) {
-        const msg = '\'revisionId\' is required to update page';
-        throw new Error(msg);
-      }
-      res = await updatePage(pageId, revisionId, markdown, options);
-    }
-
-    mutateIsEnabledUnsavedWarning(false);
-
-    return res;
-  }, [mutateIsEnabledUnsavedWarning]);
+export const updatePage = async(params: IApiv3PageUpdateParams): Promise<IApiv3PageUpdateResponse> => {
+  const res = await apiv3Put<IApiv3PageUpdateResponse>('/page', params);
+  return res.data;
 };
 };
 
 
 export type UpdateStateAfterSaveOption = {
 export type UpdateStateAfterSaveOption = {
@@ -178,7 +125,7 @@ export const useUpdateStateAfterSave = (pageId: string|undefined|null, opts?: Up
     await mutateCurrentPageId(pageId);
     await mutateCurrentPageId(pageId);
     const updatedPage = await mutateCurrentPage();
     const updatedPage = await mutateCurrentPage();
 
 
-    if (updatedPage == null) { return }
+    if (updatedPage == null || updatedPage.revision == null) { return }
 
 
     // supress to mutate only when updated from built-in editor
     // supress to mutate only when updated from built-in editor
     // and see: https://github.com/weseek/growi/pull/7118
     // and see: https://github.com/weseek/growi/pull/7118
@@ -205,17 +152,21 @@ export const unlink = async(path: string): Promise<void> => {
 };
 };
 
 
 
 
-interface PageExistRequest {
-  pagePaths: string;
-}
-
 interface PageExistResponse {
 interface PageExistResponse {
-  pages: Record<string, boolean>;
-  ok: boolean
+  isExist: boolean,
 }
 }
 
 
-export const exist = async(pagePaths: string): Promise<PageExistResponse> => {
-  const request: PageExistRequest = { pagePaths };
-  const res = await apiGet<PageExistResponse>('/pages.exist', request);
-  return res;
+export const exist = async(path: string): Promise<PageExistResponse> => {
+  const res = await apiv3Get<PageExistResponse>('/page/exist', { path });
+  return res.data;
+};
+
+export const publish = async(pageId: string): Promise<IPageHasId> => {
+  const res = await apiv3Put(`/page/${pageId}/publish`);
+  return res.data;
+};
+
+export const unpublish = async(pageId: string): Promise<IPageHasId> => {
+  const res = await apiv3Put(`/page/${pageId}/unpublish`);
+  return res.data;
 };
 };

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

@@ -1,17 +1,17 @@
 import { useCallback, useEffect } from 'react';
 import { useCallback, useEffect } from 'react';
 
 
-import EventEmitter from 'events';
+import type EventEmitter from 'events';
 
 
 import type { DrawioEditByViewerProps } from '@growi/remark-drawio';
 import type { DrawioEditByViewerProps } from '@growi/remark-drawio';
 
 
-import { useSaveOrUpdate } from '~/client/services/page-operation';
 import { replaceDrawioInMarkdown } from '~/components/Page/markdown-drawio-util-for-view';
 import { replaceDrawioInMarkdown } from '~/components/Page/markdown-drawio-util-for-view';
-import type { OptionsToSave } from '~/interfaces/page-operation';
 import { useShareLinkId } from '~/stores/context';
 import { useShareLinkId } from '~/stores/context';
 import { useDrawioModal } from '~/stores/modal';
 import { useDrawioModal } from '~/stores/modal';
-import { useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
+import { useSWRxCurrentPage } from '~/stores/page';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
+import { updatePage } from '../page-operation';
+
 
 
 const logger = loggerFactory('growi:cli:side-effects:useDrawioModalLauncherForView');
 const logger = loggerFactory('growi:cli:side-effects:useDrawioModalLauncherForView');
 
 
@@ -33,38 +33,21 @@ export const useDrawioModalLauncherForView = (opts?: {
 
 
   const { open: openDrawioModal } = useDrawioModal();
   const { open: openDrawioModal } = useDrawioModal();
 
 
-  const saveOrUpdate = useSaveOrUpdate();
-
   const saveByDrawioModal = useCallback(async(drawioMxFile: string, bol: number, eol: number) => {
   const saveByDrawioModal = useCallback(async(drawioMxFile: string, bol: number, eol: number) => {
-    if (currentPage == null || shareLinkId != null) {
+    if (currentPage == null || currentPage.revision == null || shareLinkId != null) {
       return;
       return;
     }
     }
 
 
     const currentMarkdown = currentPage.revision.body;
     const currentMarkdown = currentPage.revision.body;
     const newMarkdown = replaceDrawioInMarkdown(drawioMxFile, currentMarkdown, bol, eol);
     const newMarkdown = replaceDrawioInMarkdown(drawioMxFile, currentMarkdown, bol, eol);
 
 
-    const grantUserGroupIds = currentPage.grantedGroups.map((g) => {
-      return {
-        type: g.type,
-        item: g.item._id,
-      };
-    });
-
-    const optionsToSave: OptionsToSave = {
-      isSlackEnabled: false,
-      slackChannels: '',
-      grant: currentPage.grant,
-      // grantUserGroupIds,
-      // pageTags: tagsInfo.tags,
-    };
-
     try {
     try {
       const currentRevisionId = currentPage.revision._id;
       const currentRevisionId = currentPage.revision._id;
-      await saveOrUpdate(
-        newMarkdown,
-        { pageId: currentPage._id, path: currentPage.path, revisionId: currentRevisionId },
-        optionsToSave,
-      );
+      await updatePage({
+        pageId: currentPage._id,
+        revisionId: currentRevisionId,
+        body: newMarkdown,
+      });
 
 
       opts?.onSaveSuccess?.();
       opts?.onSaveSuccess?.();
     }
     }
@@ -72,7 +55,7 @@ export const useDrawioModalLauncherForView = (opts?: {
       logger.error('failed to save', error);
       logger.error('failed to save', error);
       opts?.onSaveError?.(error);
       opts?.onSaveError?.(error);
     }
     }
-  }, [currentPage, opts, saveOrUpdate, shareLinkId]);
+  }, [currentPage, opts, shareLinkId]);
 
 
 
 
   // set handler to open DrawioModal
   // set handler to open DrawioModal

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

@@ -1,16 +1,16 @@
 import { useCallback, useEffect } from 'react';
 import { useCallback, useEffect } from 'react';
 
 
-import EventEmitter from 'events';
+import type EventEmitter from 'events';
 
 
-import MarkdownTable from '~/client/models/MarkdownTable';
-import { useSaveOrUpdate } from '~/client/services/page-operation';
+import type MarkdownTable from '~/client/models/MarkdownTable';
 import { getMarkdownTableFromLine, replaceMarkdownTableInMarkdown } from '~/components/Page/markdown-table-util-for-view';
 import { getMarkdownTableFromLine, replaceMarkdownTableInMarkdown } from '~/components/Page/markdown-table-util-for-view';
-import type { OptionsToSave } from '~/interfaces/page-operation';
 import { useShareLinkId } from '~/stores/context';
 import { useShareLinkId } from '~/stores/context';
 import { useHandsontableModal } from '~/stores/modal';
 import { useHandsontableModal } from '~/stores/modal';
-import { useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
+import { useSWRxCurrentPage } from '~/stores/page';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
+import { updatePage } from '../page-operation';
+
 
 
 const logger = loggerFactory('growi:cli:side-effects:useHandsontableModalLauncherForView');
 const logger = loggerFactory('growi:cli:side-effects:useHandsontableModalLauncherForView');
 
 
@@ -32,38 +32,21 @@ export const useHandsontableModalLauncherForView = (opts?: {
 
 
   const { open: openHandsontableModal } = useHandsontableModal();
   const { open: openHandsontableModal } = useHandsontableModal();
 
 
-  const saveOrUpdate = useSaveOrUpdate();
-
   const saveByHandsontableModal = useCallback(async(table: MarkdownTable, bol: number, eol: number) => {
   const saveByHandsontableModal = useCallback(async(table: MarkdownTable, bol: number, eol: number) => {
-    if (currentPage == null || shareLinkId != null) {
+    if (currentPage == null || currentPage.revision == null || shareLinkId != null) {
       return;
       return;
     }
     }
 
 
     const currentMarkdown = currentPage.revision.body;
     const currentMarkdown = currentPage.revision.body;
     const newMarkdown = replaceMarkdownTableInMarkdown(table, currentMarkdown, bol, eol);
     const newMarkdown = replaceMarkdownTableInMarkdown(table, currentMarkdown, bol, eol);
 
 
-    const grantUserGroupIds = currentPage.grantedGroups.map((g) => {
-      return {
-        type: g.type,
-        item: g.item._id,
-      };
-    });
-
-    const optionsToSave: OptionsToSave = {
-      isSlackEnabled: false,
-      slackChannels: '',
-      grant: currentPage.grant,
-      // grantUserGroupIds,
-      // pageTags: tagsInfo.tags,
-    };
-
     try {
     try {
       const currentRevisionId = currentPage.revision._id;
       const currentRevisionId = currentPage.revision._id;
-      await saveOrUpdate(
-        newMarkdown,
-        { pageId: currentPage._id, path: currentPage.path, revisionId: currentRevisionId },
-        optionsToSave,
-      );
+      await updatePage({
+        pageId: currentPage._id,
+        revisionId: currentRevisionId,
+        body: newMarkdown,
+      });
 
 
       opts?.onSaveSuccess?.();
       opts?.onSaveSuccess?.();
     }
     }
@@ -71,7 +54,7 @@ export const useHandsontableModalLauncherForView = (opts?: {
       logger.error('failed to save', error);
       logger.error('failed to save', error);
       opts?.onSaveError?.(error);
       opts?.onSaveError?.(error);
     }
     }
-  }, [currentPage, opts, saveOrUpdate, shareLinkId]);
+  }, [currentPage, opts, shareLinkId]);
 
 
 
 
   // set handler to open HandsonTableModal
   // set handler to open HandsonTableModal
@@ -81,6 +64,8 @@ export const useHandsontableModalLauncherForView = (opts?: {
     }
     }
 
 
     const handler = (bol: number, eol: number) => {
     const handler = (bol: number, eol: number) => {
+      if (currentPage.revision == null) return;
+
       const markdown = currentPage.revision.body;
       const markdown = currentPage.revision.body;
       const currentMarkdownTable = getMarkdownTableFromLine(markdown, bol, eol);
       const currentMarkdownTable = getMarkdownTableFromLine(markdown, bol, eol);
       openHandsontableModal(currentMarkdownTable, false, table => saveByHandsontableModal(table, bol, eol));
       openHandsontableModal(currentMarkdownTable, false, table => saveByHandsontableModal(table, bol, eol));

+ 0 - 123
apps/app/src/client/services/use-create-page-and-transit.tsx

@@ -1,123 +0,0 @@
-import { useCallback } from 'react';
-
-import { useRouter } from 'next/router';
-
-import { createPage } from '~/client/services/page-operation';
-import { useIsNotFound, useSWRxCurrentPage } from '~/stores/page';
-import { EditorMode, useEditorMode } from '~/stores/ui';
-import loggerFactory from '~/utils/logger';
-
-const logger = loggerFactory('growi:Navbar:GrowiContextualSubNavigation');
-
-/**
- * Invoked when creation and transition has finished
- */
-type OnCreated = () => void;
-/**
- * Invoked when either creation or transition has aborted
- */
-type OnAborted = () => void;
-/**
- * Invoked when an error is occured
- */
-type OnError = (err) => void;
-/**
- * Always invoked after processing is terminated
- */
-type OnTerminated = () => void;
-
-type CreatePageAndTransitOpts = {
-  onCreationStart?: OnCreated,
-  onCreated?: OnCreated,
-  onAborted?: OnAborted,
-  onError?: OnError,
-  onTerminated?: OnTerminated,
-}
-
-type CreatePageAndTransit = (
-  pagePath: string | undefined,
-  // grant?: number,
-  // grantUserGroupId?: string,
-  opts?: CreatePageAndTransitOpts,
-) => Promise<void>;
-
-export const useCreatePageAndTransit = (): CreatePageAndTransit => {
-
-  const router = useRouter();
-
-  const { data: isNotFound } = useIsNotFound();
-  const { data: currentPage, isLoading } = useSWRxCurrentPage();
-  const { mutate: mutateEditorMode } = useEditorMode();
-
-  // const {
-  //   path: currentPagePath,
-  //   grant: currentPageGrant,
-  //   grantedGroups: currentPageGrantedGroups,
-  // } = currentPage ?? {};
-
-  return useCallback(async(pagePath, opts = {}) => {
-    if (isLoading) {
-      return;
-    }
-
-    const {
-      onCreationStart, onCreated, onAborted, onError, onTerminated,
-    } = opts;
-
-    if (isNotFound == null || !isNotFound || pagePath == null) {
-      mutateEditorMode(EditorMode.Editor);
-
-      onAborted?.();
-      onTerminated?.();
-      return;
-    }
-
-    try {
-      onCreationStart?.();
-
-      /**
-       * !! NOTICE !! - Verification of page createable or not is checked on the server side.
-       * since the new page path is not generated on the client side.
-       * need shouldGeneratePath flag.
-       */
-      // const shouldCreateUnderRoot = currentPagePath == null || currentPageGrant == null;
-      // const parentPath = shouldCreateUnderRoot
-      //   ? '/'
-      //   : currentPagePath;
-
-      // const params = {
-      //   isSlackEnabled: false,
-      //   slackChannels: '',
-      //   grant: shouldCreateUnderRoot ? 1 : currentPageGrant,
-      //   grantUserGroupIds: shouldCreateUnderRoot ? undefined : currentPageGrantedGroups,
-      //   shouldGeneratePath: true,
-      // };
-
-      // !! NOTICE !! - if shouldGeneratePath is flagged, send the parent page path
-      // const response = await createPage(parentPath, '', params);
-
-      const params = {
-        isSlackEnabled: false,
-        slackChannels: '',
-        grant: 4,
-        // grant,
-        // grantUserGroupId,
-      };
-
-      const response = await createPage(pagePath, '', params);
-
-      await router.push(`${response.page.id}#edit`);
-      mutateEditorMode(EditorMode.Editor);
-
-      onCreated?.();
-    }
-    catch (err) {
-      logger.warn(err);
-      onError?.(err);
-    }
-    finally {
-      onTerminated?.();
-    }
-
-  }, [isLoading, isNotFound, mutateEditorMode, router]);
-};

+ 0 - 55
apps/app/src/client/services/use-on-template-button-clicked.ts

@@ -1,55 +0,0 @@
-import { useCallback, useState } from 'react';
-
-import { isCreatablePage } from '@growi/core/dist/utils/page-path-utils';
-import { useRouter } from 'next/router';
-
-import { createPage, exist } from '~/client/services/page-operation';
-import type { LabelType } from '~/interfaces/template';
-
-export const useOnTemplateButtonClicked = (
-    currentPagePath?: string,
-    isLoading?: boolean,
-): {
-  onClickHandler: (label: LabelType) => Promise<void>,
-  isPageCreating: boolean
-} => {
-  const router = useRouter();
-  const [isPageCreating, setIsPageCreating] = useState(false);
-
-  const onClickHandler = useCallback(async(label: LabelType) => {
-    if (isLoading) return;
-
-    try {
-      setIsPageCreating(true);
-
-      const targetPath = currentPagePath == null || currentPagePath === '/'
-        ? `/${label}`
-        : `${currentPagePath}/${label}`;
-
-      const path = isCreatablePage(targetPath) ? targetPath : `/${label}`;
-
-      const res = await exist(JSON.stringify([path]));
-      if (!res.pages[path]) {
-        const params = {
-          isSlackEnabled: false,
-          slackChannels: '',
-          grant: 4,
-        // grant: currentPage?.grant || 1,
-        // grantUserGroupId: currentPage?.grantedGroup?._id,
-        };
-
-        await createPage(path, '', params);
-      }
-
-      router.push(`${path}#edit`);
-    }
-    catch (err) {
-      throw err;
-    }
-    finally {
-      setIsPageCreating(false);
-    }
-  }, [currentPagePath, isLoading, router]);
-
-  return { onClickHandler, isPageCreating };
-};

+ 2 - 2
apps/app/src/client/util/bookmark-utils.ts

@@ -1,6 +1,6 @@
 import type { IRevision, Ref } from '@growi/core';
 import type { IRevision, Ref } from '@growi/core';
 
 
-import { BookmarkFolderItems } from '~/interfaces/bookmark-info';
+import type { BookmarkFolderItems } from '~/interfaces/bookmark-info';
 
 
 import { apiv3Delete, apiv3Post, apiv3Put } from './apiv3-client';
 import { apiv3Delete, apiv3Post, apiv3Put } from './apiv3-client';
 
 
@@ -31,7 +31,7 @@ export const deleteBookmarkFolder = async(bookmarkFolderId: string): Promise<voi
 };
 };
 
 
 // Rename page from bookmark item control
 // Rename page from bookmark item control
-export const renamePage = async(pageId: string, revisionId: Ref<IRevision>, newPagePath: string): Promise<void> => {
+export const renamePage = async(pageId: string, revisionId: Ref<IRevision> | undefined, newPagePath: string): Promise<void> => {
   await apiv3Put('/pages/rename', { pageId, revisionId, newPagePath });
   await apiv3Put('/pages/rename', { pageId, revisionId, newPagePath });
 };
 };
 
 

+ 9 - 10
apps/app/src/components/Admin/AuditLog/SearchUsernameTypeahead.tsx

@@ -1,11 +1,13 @@
+import type { ForwardRefRenderFunction } from 'react';
 import React, {
 import React, {
-  Fragment, useState, useCallback, useRef, ForwardRefRenderFunction, forwardRef, useImperativeHandle,
+  Fragment, useState, useCallback, forwardRef, useRef, useImperativeHandle,
 } from 'react';
 } from 'react';
 
 
+import type { TypeaheadRef } from 'react-bootstrap-typeahead';
 import { AsyncTypeahead, Menu, MenuItem } from 'react-bootstrap-typeahead';
 import { AsyncTypeahead, Menu, MenuItem } from 'react-bootstrap-typeahead';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
-import { IClearable } from '~/client/interfaces/clearable';
+import type { IClearable } from '~/client/interfaces/clearable';
 import { useSWRxUsernames } from '~/stores/user';
 import { useSWRxUsernames } from '~/stores/user';
 
 
 
 
@@ -30,7 +32,7 @@ const SearchUsernameTypeaheadSubstance: ForwardRefRenderFunction<IClearable, Pro
   const { onChange } = props;
   const { onChange } = props;
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
-  const typeaheadRef = useRef<IClearable>(null);
+  const typeaheadRef = useRef<TypeaheadRef>(null);
 
 
   /*
   /*
    * State
    * State
@@ -41,11 +43,11 @@ const SearchUsernameTypeaheadSubstance: ForwardRefRenderFunction<IClearable, Pro
    * Fetch
    * Fetch
    */
    */
   const requestOptions = { isIncludeActiveUser: true, isIncludeInactiveUser: true, isIncludeActivitySnapshotUser: true };
   const requestOptions = { isIncludeActiveUser: true, isIncludeInactiveUser: true, isIncludeActivitySnapshotUser: true };
-  const { data: usernameData, error } = useSWRxUsernames(searchKeyword, 0, 5, requestOptions);
+  const { data: usernameData, error, isLoading: _isLoading } = useSWRxUsernames(searchKeyword, 0, 5, requestOptions);
   const activeUsernames = usernameData?.activeUser?.usernames != null ? usernameData.activeUser.usernames : [];
   const activeUsernames = usernameData?.activeUser?.usernames != null ? usernameData.activeUser.usernames : [];
   const inactiveUsernames = usernameData?.inactiveUser?.usernames != null ? usernameData.inactiveUser.usernames : [];
   const inactiveUsernames = usernameData?.inactiveUser?.usernames != null ? usernameData.inactiveUser.usernames : [];
   const activitySnapshotUsernames = usernameData?.activitySnapshotUser?.usernames != null ? usernameData.activitySnapshotUser.usernames : [];
   const activitySnapshotUsernames = usernameData?.activitySnapshotUser?.usernames != null ? usernameData.activitySnapshotUser.usernames : [];
-  const isLoading = usernameData === undefined && error == null;
+  const isLoading = _isLoading === true && error == null;
 
 
   const allUser: UserDataType[] = [];
   const allUser: UserDataType[] = [];
   const pushToAllUser = (usernames: string[], category: CategoryType) => {
   const pushToAllUser = (usernames: string[], category: CategoryType) => {
@@ -59,10 +61,8 @@ const SearchUsernameTypeaheadSubstance: ForwardRefRenderFunction<IClearable, Pro
    * Functions
    * Functions
    */
    */
   const changeHandler = useCallback((userData: UserDataType[]) => {
   const changeHandler = useCallback((userData: UserDataType[]) => {
-    if (onChange != null) {
-      const usernames = userData.map(user => user.username);
-      onChange(usernames);
-    }
+    const usernames = userData.map(user => user.username);
+    onChange(usernames);
   }, [onChange]);
   }, [onChange]);
 
 
   const searchHandler = useCallback((text: string) => {
   const searchHandler = useCallback((text: string) => {
@@ -120,7 +120,6 @@ const SearchUsernameTypeaheadSubstance: ForwardRefRenderFunction<IClearable, Pro
         delay={400}
         delay={400}
         minLength={0}
         minLength={0}
         placeholder={t('admin:audit_log_management.username')}
         placeholder={t('admin:audit_log_management.username')}
-        caseSensitive={false}
         isLoading={isLoading}
         isLoading={isLoading}
         options={allUser}
         options={allUser}
         onSearch={searchHandler}
         onSearch={searchHandler}

+ 1 - 1
apps/app/src/components/Admin/Common/AdminNavigation.tsx

@@ -46,7 +46,7 @@ const MenuLink = ({
 }: MenuLinkProps) => {
 }: MenuLinkProps) => {
 
 
   const pageTransitionClassName = isListGroupItems
   const pageTransitionClassName = isListGroupItems
-    ? 'list-group-item list-group-item-action border-0 round-corner'
+    ? 'list-group-item list-group-item-action rounded border-0'
     : 'dropdown-item px-3 py-2';
     : 'dropdown-item px-3 py-2';
 
 
   const href = isRoot ? '/admin' : urljoin('/admin', menu);
   const href = isRoot ? '/admin' : urljoin('/admin', menu);

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

@@ -87,10 +87,9 @@ class GitHubSecurityManagementContents extends React.Component {
             <p className="form-text text-muted small">{t('security_settings.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
             <p className="form-text text-muted small">{t('security_settings.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
             {(siteUrl == null || siteUrl === '') && (
             {(siteUrl == null || siteUrl === '') && (
               <div className="alert alert-danger">
               <div className="alert alert-danger">
-                <i
-                  className="icon-exclamation"
-                  // eslint-disable-next-line max-len
-                  dangerouslySetInnerHTML={{ __html: t('alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<i class="icon-login"></i></a>`, ns: 'commons' }) }}
+                <span className="material-symbols-outlined">error</span>
+                <span // eslint-disable-next-line max-len
+                  dangerouslySetInnerHTML={{ __html: t('alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<span class="material-symbols-outlined">login</span></a>`, ns: 'commons' }) }}
                 />
                 />
               </div>
               </div>
             )}
             )}
@@ -172,7 +171,7 @@ class GitHubSecurityManagementContents extends React.Component {
 
 
         <div style={{ minHeight: '300px' }}>
         <div style={{ minHeight: '300px' }}>
           <h4>
           <h4>
-            <i className="icon-question" aria-hidden="true"></i>
+            <span className="material-symbols-outlined" aria-hidden="true">help</span>
             <a href="#collapseHelpForGitHubOauth" data-bs-toggle="collapse"> {t('security_settings.OAuth.how_to.github')}</a>
             <a href="#collapseHelpForGitHubOauth" data-bs-toggle="collapse"> {t('security_settings.OAuth.how_to.github')}</a>
           </h4>
           </h4>
           <ol id="collapseHelpForGitHubOauth" className="collapse">
           <ol id="collapseHelpForGitHubOauth" className="collapse">

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

@@ -85,10 +85,10 @@ class GoogleSecurityManagementContents extends React.Component {
             <p className="form-text text-muted small">{t('security_settings.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
             <p className="form-text text-muted small">{t('security_settings.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
             {(siteUrl == null || siteUrl === '') && (
             {(siteUrl == null || siteUrl === '') && (
               <div className="alert alert-danger">
               <div className="alert alert-danger">
-                <i
-                  className="icon-exclamation"
-                  // eslint-disable-next-line max-len
-                  dangerouslySetInnerHTML={{ __html: t('alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<i class="icon-login"></i></a>`, ns: 'commons' }) }}
+                <span className="material-symbols-outlined">error</span>
+                <span
+                // eslint-disable-next-line max-len
+                  dangerouslySetInnerHTML={{ __html: t('alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<span class="material-symbols-outlined">login</span></a>`, ns: 'commons' }) }}
                 />
                 />
               </div>
               </div>
             )}
             )}

+ 6 - 6
apps/app/src/components/Admin/Security/OidcSecuritySettingContents.jsx

@@ -79,10 +79,10 @@ class OidcSecurityManagementContents extends React.Component {
             <p className="form-text text-muted small">{t('security_settings.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
             <p className="form-text text-muted small">{t('security_settings.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
             {(siteUrl == null || siteUrl === '') && (
             {(siteUrl == null || siteUrl === '') && (
               <div className="alert alert-danger">
               <div className="alert alert-danger">
-                <i
-                  className="icon-exclamation"
+                <span className="material-symbols-outlined">error</span>
+                <span
                   // eslint-disable-next-line max-len
                   // eslint-disable-next-line max-len
-                  dangerouslySetInnerHTML={{ __html: t('alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<i class="icon-login"></i></a>`, ns: 'commons' }) }}
+                  dangerouslySetInnerHTML={{ __html: t('alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<span class="material-symbols-outlined">login</span></a>`, ns: 'commons' }) }}
                 />
                 />
               </div>
               </div>
             )}
             )}
@@ -375,10 +375,10 @@ class OidcSecurityManagementContents extends React.Component {
                 <p className="form-text text-muted small">{t('security_settings.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
                 <p className="form-text text-muted small">{t('security_settings.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
                 {(siteUrl == null || siteUrl === '') && (
                 {(siteUrl == null || siteUrl === '') && (
                   <div className="alert alert-danger">
                   <div className="alert alert-danger">
-                    <i
-                      className="icon-exclamation"
+                    <span className="material-symbols-outlined">error</span>
+                    <span
                       // eslint-disable-next-line max-len
                       // eslint-disable-next-line max-len
-                      dangerouslySetInnerHTML={{ __html: t('alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<i class="icon-login"></i></a>`, ns: 'commons' }) }}
+                      dangerouslySetInnerHTML={{ __html: t('alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<span class="material-symbols-outlined">login</span></a>`, ns: 'commons' }) }}
                     />
                     />
                   </div>
                   </div>
                 )}
                 )}

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

@@ -96,10 +96,10 @@ class SamlSecurityManagementContents extends React.Component {
             <p className="form-text text-muted small">{t('security_settings.desc_of_callback_URL', { AuthName: 'SAML Identity' })}</p>
             <p className="form-text text-muted small">{t('security_settings.desc_of_callback_URL', { AuthName: 'SAML Identity' })}</p>
             {(siteUrl == null || siteUrl === '') && (
             {(siteUrl == null || siteUrl === '') && (
               <div className="alert alert-danger">
               <div className="alert alert-danger">
-                <i
-                  className="icon-exclamation"
+                <span className="material-symbols-outlined">error</span>
+                <span
                   // eslint-disable-next-line max-len
                   // eslint-disable-next-line max-len
-                  dangerouslySetInnerHTML={{ __html: t('alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<i class="icon-login"></i></a>`, ns: 'commons' }) }}
+                  dangerouslySetInnerHTML={{ __html: t('alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<span class="material-symbols-outlined">login</span></a>`, ns: 'commons' }) }}
                 />
                 />
               </div>
               </div>
             )}
             )}
@@ -484,7 +484,11 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                               aria-expanded="true"
                               aria-expanded="true"
                               aria-controls="ablchelp"
                               aria-controls="ablchelp"
                             >
                             >
-                              <i className={`icon-fw ${this.state.isHelpOpened ? 'icon-arrow-down' : 'icon-arrow-right'} small`}></i> Show more...
+                              <span
+                                className="material-symbols-outlined me-1"
+                                small
+                              >{this.state.isHelpOpened ? 'expand_more' : 'chevron_right'}
+                              </span> Show more...
                             </button>
                             </button>
                           </h2>
                           </h2>
                           <Collapse isOpen={this.state.isHelpOpened}>
                           <Collapse isOpen={this.state.isHelpOpened}>

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

@@ -81,7 +81,7 @@ const SecurityManagementContents = () => {
             href="/admin/markdown/#preventXSS"
             href="/admin/markdown/#preventXSS"
             style={{ fontSize: 'large' }}
             style={{ fontSize: 'large' }}
           >
           >
-            <i className="fa-fw icon-login"></i> {t('security_settings.xss_prevent_setting_link')}
+            <span className="material-symbols-outlined me-1">login</span> {t('security_settings.xss_prevent_setting_link')}
           </Link>
           </Link>
         </div>
         </div>
       </div>
       </div>

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

@@ -303,7 +303,7 @@ class SecuritySetting extends React.Component {
                     <div className="pb-4">
                     <div className="pb-4">
                       <p className="card custom-card">
                       <p className="card custom-card">
                         <span className="text-warning">
                         <span className="text-warning">
-                          <i className="icon-info"></i>
+                          <span className="material-symbols-outlined">info</span>
                           {/* eslint-disable-next-line react/no-danger */}
                           {/* eslint-disable-next-line react/no-danger */}
                           <span dangerouslySetInnerHTML={{ __html: t('security_settings.page_delete_rights_caution') }} />
                           <span dangerouslySetInnerHTML={{ __html: t('security_settings.page_delete_rights_caution') }} />
                         </span>
                         </span>
@@ -368,11 +368,11 @@ class SecuritySetting extends React.Component {
             <tbody>
             <tbody>
               <tr>
               <tr>
                 <th scope="row">{ t('public') }</th>
                 <th scope="row">{ t('public') }</th>
-                <td><i className="icon-fw icon-check text-success"></i>{ t('security_settings.always_displayed') }</td>
+                <td><span className="material-symbols-outlined text-success me-1">check_circle</span>{ t('security_settings.always_displayed') }</td>
               </tr>
               </tr>
               <tr>
               <tr>
                 <th scope="row">{ t('anyone_with_the_link') }</th>
                 <th scope="row">{ t('anyone_with_the_link') }</th>
-                <td><i className="icon-fw icon-ban text-danger"></i>{ t('security_settings.always_hidden') }</td>
+                <td><span className="material-symbols-outlined text-danger me-1">cancel</span>{ t('security_settings.always_hidden') }</td>
               </tr>
               </tr>
               <tr>
               <tr>
                 <th scope="row">{ t('only_me') }</th>
                 <th scope="row">{ t('only_me') }</th>
@@ -444,8 +444,8 @@ class SecuritySetting extends React.Component {
             </div>
             </div>
             {adminGeneralSecurityContainer.isWikiModeForced && (
             {adminGeneralSecurityContainer.isWikiModeForced && (
               <p className="alert alert-warning mt-2 col-6">
               <p className="alert alert-warning mt-2 col-6">
-                <i className="icon-exclamation icon-fw">
-                </i><b>FIXED</b><br />
+                <span className="material-symbols-outlined me-1">error</span>
+                <b>FIXED</b><br />
                 <b
                 <b
                   dangerouslySetInnerHTML={{
                   dangerouslySetInnerHTML={{
                     __html: t('security_settings.Fixed by env var',
                     __html: t('security_settings.Fixed by env var',
@@ -526,7 +526,7 @@ class SecuritySetting extends React.Component {
             <p className="form-text text-muted" dangerouslySetInnerHTML={{ __html: t('security_settings.max_age_desc') }} />
             <p className="form-text text-muted" dangerouslySetInnerHTML={{ __html: t('security_settings.max_age_desc') }} />
             <p className="card custom-card">
             <p className="card custom-card">
               <span className="text-warning">
               <span className="text-warning">
-                <i className="icon-info"></i> {t('security_settings.max_age_caution')}
+                <span className="material-symbols-outlined">info</span> {t('security_settings.max_age_caution')}
               </span>
               </span>
             </p>
             </p>
           </div>
           </div>

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

@@ -15,14 +15,14 @@ const ProxyCircle = () => (
 
 
 const BridgeCore = (props) => {
 const BridgeCore = (props) => {
   const {
   const {
-    description, iconClass, hrClass, withProxy,
+    description, iconClass, iconName, hrClass, withProxy,
   } = props;
   } = props;
 
 
   return (
   return (
     <>
     <>
       <div id="grw-bridge-container" className={`grw-bridge-container ${withProxy ? 'with-proxy' : ''}`}>
       <div id="grw-bridge-container" className={`grw-bridge-container ${withProxy ? 'with-proxy' : ''}`}>
         <p className={`${withProxy ? 'mt-0' : 'mt-2'}`}>
         <p className={`${withProxy ? 'mt-0' : 'mt-2'}`}>
-          <i className={iconClass} />
+          <span className={iconClass}>{iconName}</span>
           <small
           <small
             className="ms-2 d-none d-lg-inline"
             className="ms-2 d-none d-lg-inline"
             // eslint-disable-next-line react/no-danger
             // eslint-disable-next-line react/no-danger
@@ -47,6 +47,7 @@ const BridgeCore = (props) => {
 BridgeCore.propTypes = {
 BridgeCore.propTypes = {
   description: PropTypes.string.isRequired,
   description: PropTypes.string.isRequired,
   iconClass: PropTypes.string.isRequired,
   iconClass: PropTypes.string.isRequired,
+  iconName: PropTypes.string.isRequired,
   hrClass: PropTypes.string.isRequired,
   hrClass: PropTypes.string.isRequired,
   withProxy: PropTypes.bool,
   withProxy: PropTypes.bool,
 };
 };
@@ -58,24 +59,28 @@ const Bridge = (props) => {
 
 
   let description;
   let description;
   let iconClass;
   let iconClass;
+  let iconName;
   let hrClass;
   let hrClass;
 
 
   // empty or all failed
   // empty or all failed
   if (totalCount === 0 || errorCount === totalCount) {
   if (totalCount === 0 || errorCount === totalCount) {
     description = t('admin:slack_integration.integration_sentence.integration_is_not_complete');
     description = t('admin:slack_integration.integration_sentence.integration_is_not_complete');
-    iconClass = 'icon-info text-danger';
+    iconClass = 'material-symbols-outlined text-danger';
+    iconName = 'info';
     hrClass = 'border-danger admin-border-failed';
     hrClass = 'border-danger admin-border-failed';
   }
   }
   // all green
   // all green
   else if (errorCount === 0) {
   else if (errorCount === 0) {
     description = t('admin:slack_integration.integration_sentence.integration_successful');
     description = t('admin:slack_integration.integration_sentence.integration_successful');
     iconClass = 'fa fa-check text-success';
     iconClass = 'fa fa-check text-success';
+    iconName = '';
     hrClass = 'border-success admin-border-success';
     hrClass = 'border-success admin-border-success';
   }
   }
   // some of them failed
   // some of them failed
   else {
   else {
     description = t('admin:slack_integration.integration_sentence.integration_some_ws_is_not_complete');
     description = t('admin:slack_integration.integration_sentence.integration_some_ws_is_not_complete');
     iconClass = 'fa fa-check text-warning';
     iconClass = 'fa fa-check text-warning';
+    iconName = '';
     hrClass = 'border-warning admin-border-failed';
     hrClass = 'border-warning admin-border-failed';
   }
   }
 
 
@@ -83,6 +88,7 @@ const Bridge = (props) => {
     <BridgeCore
     <BridgeCore
       description={description}
       description={description}
       iconClass={iconClass}
       iconClass={iconClass}
+      iconName={iconName}
       hrClass={hrClass}
       hrClass={hrClass}
       withProxy={withProxy}
       withProxy={withProxy}
     />
     />

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

@@ -142,7 +142,7 @@ const CustomBotWithoutProxySettingsAccordion = (props) => {
       >
       >
         <p className="text-center m-4">{t('admin:slack_integration.accordion.test_connection_by_pressing_button')}</p>
         <p className="text-center m-4">{t('admin:slack_integration.accordion.test_connection_by_pressing_button')}</p>
         <p className="text-center text-warning">
         <p className="text-center text-warning">
-          <i className="icon-info">{t('admin:slack_integration.accordion.test_connection_only_public_channel')}</i>
+          <span className="material-symbols-outlined">info</span>{t('admin:slack_integration.accordion.test_connection_only_public_channel')}
         </p>
         </p>
         <div className="d-flex justify-content-center">
         <div className="d-flex justify-content-center">
           <form className="align-items-center" onSubmit={e => submitForm(e)}>
           <form className="align-items-center" onSubmit={e => submitForm(e)}>

+ 1 - 1
apps/app/src/components/Admin/SlackIntegration/SlackIntegration.jsx

@@ -211,7 +211,7 @@ const SlackIntegration = () => {
         <h2 className="admin-setting-header mb-4">
         <h2 className="admin-setting-header mb-4">
           {t('admin:slack_integration.selecting_bot_types.slack_bot')}
           {t('admin:slack_integration.selecting_bot_types.slack_bot')}
           <a className="ms-2 btn-link small" href={t('admin:slack_integration.docs_url.slack_integration')} target="_blank" rel="noopener noreferrer">
           <a className="ms-2 btn-link small" href={t('admin:slack_integration.docs_url.slack_integration')} target="_blank" rel="noopener noreferrer">
-            <i className="icon icon-question ms-1" aria-hidden="true"></i>
+            <span className="material-symbols-outlined ms-1" aria-hidden="true">help</span>
           </a>
           </a>
         </h2>
         </h2>
 
 

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

@@ -240,7 +240,7 @@ const TestProcess = ({
     <>
     <>
       <p className="text-center m-4">{t('admin:slack_integration.accordion.test_connection_by_pressing_button')}</p>
       <p className="text-center m-4">{t('admin:slack_integration.accordion.test_connection_by_pressing_button')}</p>
       <p className="text-center text-warning">
       <p className="text-center text-warning">
-        <i className="icon-info">{t('admin:slack_integration.accordion.test_connection_only_public_channel')}</i>
+        <span className="material-symbols-outlined me-1">info</span>{t('admin:slack_integration.accordion.test_connection_only_public_channel')}
       </p>
       </p>
       <div className="d-flex justify-content-center">
       <div className="d-flex justify-content-center">
         <form className="justify-content-center" onSubmit={e => submitForm(e)}>
         <form className="justify-content-center" onSubmit={e => submitForm(e)}>

+ 2 - 3
apps/app/src/components/Admin/UserGroup/UserGroupModal.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 type { Ref, IUserGroup, IUserGroupHasId } from '@growi/core';
 import type { Ref, IUserGroup, IUserGroupHasId } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';

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

@@ -1,13 +1,12 @@
-import React, {
-  FC, useState, useEffect,
-} from 'react';
+import type { FC } from 'react';
+import React, { useState, useEffect } from 'react';
 
 
 import type { IUserGroupHasId, IUserGroupRelation, IUserHasId } from '@growi/core';
 import type { IUserGroupHasId, IUserGroupRelation, IUserHasId } from '@growi/core';
 import dateFnsFormat from 'date-fns/format';
 import dateFnsFormat from 'date-fns/format';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import Link from 'next/link';
 import Link from 'next/link';
 
 
-import { IExternalUserGroupHasId } from '~/features/external-user-group/interfaces/external-user-group';
+import type { IExternalUserGroupHasId } from '~/features/external-user-group/interfaces/external-user-group';
 
 
 
 
 type Props = {
 type Props = {
@@ -206,11 +205,11 @@ export const UserGroupTable: FC<Props> = ({
                           className="btn btn-outline-secondary btn-sm dropdown-toggle"
                           className="btn btn-outline-secondary btn-sm dropdown-toggle"
                           data-bs-toggle="dropdown"
                           data-bs-toggle="dropdown"
                         >
                         >
-                          <i className="icon-settings"></i>
+                          <span className="material-symbols-outlined fs-5">settings</span>
                         </button>
                         </button>
                         <div className="dropdown-menu" role="menu" aria-labelledby={`admin-group-menu-button-${group._id}`}>
                         <div className="dropdown-menu" role="menu" aria-labelledby={`admin-group-menu-button-${group._id}`}>
                           <button className="dropdown-item" type="button" role="button" onClick={onClickEdit} data-user-group-id={group._id}>
                           <button className="dropdown-item" type="button" role="button" onClick={onClickEdit} data-user-group-id={group._id}>
-                            <i className="icon-fw icon-note"></i> {t('Edit')}
+                            <span className="material-symbols-outlined me-1">edit_square</span> {t('Edit')}
                           </button>
                           </button>
                           {onRemove != null
                           {onRemove != null
                           && (
                           && (

+ 4 - 3
apps/app/src/components/Admin/UserGroupDetail/UpdateParentConfirmModal.tsx

@@ -1,4 +1,5 @@
-import React, { FC, useState } from 'react';
+import type { FC } from 'react';
+import React, { useState } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import {
 import {
@@ -27,7 +28,7 @@ export const UpdateParentConfirmModal: FC = () => {
   return (
   return (
     <Modal className="modal-md" isOpen={isOpened} toggle={closeModal}>
     <Modal className="modal-md" isOpen={isOpened} toggle={closeModal}>
       <ModalHeader tag="h4" toggle={closeModal} className="bg-warning text-light">
       <ModalHeader tag="h4" toggle={closeModal} className="bg-warning text-light">
-        <i className="icon icon-warning"></i> {t('admin:user_group_management.update_parent_confirm_modal.header')}
+        <span className="material-symbols-outlined">warning</span> {t('admin:user_group_management.update_parent_confirm_modal.header')}
       </ModalHeader>
       </ModalHeader>
       {
       {
         targetGroup != null && updateData != null ? (
         targetGroup != null && updateData != null ? (
@@ -39,7 +40,7 @@ export const UpdateParentConfirmModal: FC = () => {
                 {t('admin:user_group_management.update_parent_confirm_modal.caution_change_parent', { groupName: targetGroup.name })}
                 {t('admin:user_group_management.update_parent_confirm_modal.caution_change_parent', { groupName: targetGroup.name })}
               </div>
               </div>
               <div className="text-danger mb-3">
               <div className="text-danger mb-3">
-                <i className="icon-exclamation"></i>
+                <span className="material-symbols-outlined">error</span>
                 {t('admin:user_group_management.update_parent_confirm_modal.danger_message')}
                 {t('admin:user_group_management.update_parent_confirm_modal.danger_message')}
               </div>
               </div>
 
 

+ 0 - 169
apps/app/src/components/Admin/UserGroupDetail/UserGroupUserFormByInput.jsx

@@ -1,169 +0,0 @@
-import React from 'react';
-
-import { UserPicture } from '@growi/ui/dist/components';
-import { useTranslation } from 'next-i18next';
-import PropTypes from 'prop-types';
-import { AsyncTypeahead } from 'react-bootstrap-typeahead';
-import { debounce } from 'throttle-debounce';
-
-import { toastSuccess, toastError } from '~/client/util/toastr';
-import Xss from '~/services/xss';
-
-class UserGroupUserFormByInput extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      keyword: '',
-      inputUser: '',
-      applicableUsers: [],
-      isLoading: false,
-      searchError: null,
-    };
-
-    this.xss = new Xss();
-
-    this.addUserBySubmit = this.addUserBySubmit.bind(this);
-    this.validateForm = this.validateForm.bind(this);
-    this.handleChange = this.handleChange.bind(this);
-    this.handleSearch = this.handleSearch.bind(this);
-    this.onKeyDown = this.onKeyDown.bind(this);
-    this.renderMenuItemChildren = this.renderMenuItemChildren.bind(this);
-
-    this.searhApplicableUsersDebounce = debounce(1000, this.searhApplicableUsers);
-  }
-
-  async addUserBySubmit() {
-    const { userGroup, onClickAddUserBtn } = this.props;
-
-    if (this.state.inputUser.length === 0) { return }
-    const userName = this.state.inputUser[0].username;
-
-    try {
-      await onClickAddUserBtn(userName);
-      toastSuccess(`Added "${this.xss.process(userName)}" to "${this.xss.process(userGroup.name)}"`);
-      this.setState({ inputUser: '' });
-    }
-    catch (err) {
-      toastError(new Error(`Unable to add "${this.xss.process(userName)}" to "${this.xss.process(userGroup.name)}"`));
-    }
-
-
-  }
-
-  validateForm() {
-    return this.state.inputUser !== '';
-  }
-
-  async searhApplicableUsers() {
-    const { onSearchApplicableUsers } = this.props;
-
-    try {
-      const users = await onSearchApplicableUsers(this.state.keyword);
-      this.setState({ applicableUsers: users, isLoading: false });
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }
-
-  /**
-   * Reflect when forecast is clicked
-   * @param {object} inputUser
-   */
-  handleChange(inputUser) {
-    this.setState({ inputUser });
-  }
-
-  handleSearch(keyword) {
-    if (keyword === '') {
-      return;
-    }
-
-    this.setState({ keyword, isLoading: true });
-    this.searhApplicableUsersDebounce();
-  }
-
-  onKeyDown(event) {
-    // 13 is Enter key
-    if (event.keyCode === 13) {
-      this.addUserBySubmit();
-    }
-  }
-
-  renderMenuItemChildren(option) {
-    const { isAlsoNameSearched, isAlsoMailSearched } = this.props;
-    const user = option;
-    return (
-      <>
-        <UserPicture user={user} size="sm" noLink noTooltip />
-        <strong className="ms-2">{user.username}</strong>
-        {isAlsoNameSearched && <span className="ms-2">{user.name}</span>}
-        {isAlsoMailSearched && <span className="ms-2">{user.email}</span>}
-      </>
-    );
-  }
-
-  getEmptyLabel() {
-    return (this.state.searchError !== null) && 'Error on searching.';
-  }
-
-  render() {
-    const { t } = this.props;
-
-    const inputProps = { autoComplete: 'off' };
-
-    return (
-      <div className="row">
-        <div className="col-8 pe-0">
-          <AsyncTypeahead
-            {...this.props}
-            id="name-typeahead-asynctypeahead"
-            ref={(c) => { this.typeahead = c }}
-            inputProps={inputProps}
-            isLoading={this.state.isLoading}
-            labelKey={user => `${user.username} ${user.name} ${user.email}`}
-            minLength={0}
-            options={this.state.applicableUsers} // Search result
-            searchText={(this.state.isLoading ? 'Searching...' : this.getEmptyLabel())}
-            renderMenuItemChildren={this.renderMenuItemChildren}
-            align="left"
-            onChange={this.handleChange}
-            onSearch={this.handleSearch}
-            onKeyDown={this.onKeyDown}
-            caseSensitive={false}
-            clearButton
-          />
-        </div>
-        <div className="col-2 ps-0">
-          <button
-            type="button"
-            className="btn btn-success"
-            disabled={!this.validateForm()}
-            onClick={this.addUserBySubmit}
-          >
-            {t('add')}
-          </button>
-        </div>
-      </div>
-    );
-  }
-
-}
-
-UserGroupUserFormByInput.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  isAlsoMailSearched: PropTypes.bool.isRequired,
-  isAlsoNameSearched: PropTypes.bool.isRequired,
-  onClickAddUserBtn: PropTypes.func,
-  onSearchApplicableUsers: PropTypes.func,
-  userGroup: PropTypes.object,
-};
-
-const UserGroupUserFormByInputWrapperFC = (props) => {
-  const { t } = useTranslation();
-  return <UserGroupUserFormByInput t={t} {...props} />;
-};
-
-export default UserGroupUserFormByInputWrapperFC;

+ 122 - 0
apps/app/src/components/Admin/UserGroupDetail/UserGroupUserFormByInput.tsx

@@ -0,0 +1,122 @@
+import type { FC, KeyboardEvent } from 'react';
+import React, { useState, useRef } from 'react';
+
+import type { IUserGroupHasId, IUserHasId } from '@growi/core';
+import { UserPicture } from '@growi/ui/dist/components';
+import { useTranslation } from 'next-i18next';
+import { AsyncTypeahead } from 'react-bootstrap-typeahead';
+
+import { toastSuccess, toastError } from '~/client/util/toastr';
+import type { SearchType } from '~/interfaces/user-group';
+import Xss from '~/services/xss';
+
+type Props = {
+  userGroup: IUserGroupHasId,
+  onClickAddUserBtn: (username: string) => Promise<void>,
+  onSearchApplicableUsers: (searchWord: string) => Promise<IUserHasId[]>,
+  isAlsoNameSearched: boolean,
+  isAlsoMailSearched: boolean,
+  searchType: SearchType,
+}
+
+export const UserGroupUserFormByInput: FC<Props> = (props) => {
+  const {
+    userGroup, onClickAddUserBtn, onSearchApplicableUsers, isAlsoNameSearched, isAlsoMailSearched, searchType,
+  } = props;
+
+  const { t } = useTranslation();
+  const typeaheadRef = useRef(null);
+  const [inputUser, setInputUser] = useState<IUserHasId[]>([]);
+  const [applicableUsers, setApplicableUsers] = useState<IUserHasId[]>([]);
+  const [isLoading, setIsLoading] = useState(false);
+  const [isSearchError, setIsSearchError] = useState(false);
+
+  const xss = new Xss();
+
+  const addUserBySubmit = async() => {
+    if (inputUser.length === 0) { return }
+    const userName = inputUser[0].username;
+
+    try {
+      await onClickAddUserBtn(userName);
+      toastSuccess(`Added "${xss.process(userName)}" to "${xss.process(userGroup.name)}"`);
+      setInputUser([]);
+    }
+    catch (err) {
+      toastError(new Error(`Unable to add "${xss.process(userName)}" to "${xss.process(userGroup.name)}"`));
+    }
+  };
+
+  const searchApplicableUsers = async(keyword: string) => {
+    try {
+      const users = await onSearchApplicableUsers(keyword);
+      setApplicableUsers(users);
+      setIsLoading(false);
+    }
+    catch (err) {
+      setIsSearchError(true);
+      toastError(err);
+    }
+  };
+
+  const handleChange = (inputUser: IUserHasId[]) => {
+    setInputUser(inputUser);
+  };
+
+  const handleSearch = async(keyword: string) => {
+    setIsLoading(true);
+    await searchApplicableUsers(keyword);
+  };
+
+  const onKeyDown = (event: KeyboardEvent) => {
+    if (event.key === 'Enter') {
+      addUserBySubmit();
+    }
+  };
+
+  const renderMenuItemChildren = (option: IUserHasId) => {
+    const user = option;
+
+    return (
+      <>
+        <UserPicture user={user} size="sm" noLink noTooltip />
+        <strong className="ms-2">{user.username}</strong>
+        {isAlsoNameSearched && <span className="ms-2">{user.name}</span>}
+        {isAlsoMailSearched && <span className="ms-2">{user.email}</span>}
+      </>
+    );
+  };
+
+  return (
+    <div className="row">
+      <div className="col-8 pe-0">
+        <AsyncTypeahead
+          key={`${searchType}-${isAlsoNameSearched}-${isAlsoMailSearched}`} // The searched keywords are not re-searched, so re-rendered by key.
+          id="name-typeahead-asynctypeahead"
+          inputProps={{ autoComplete: 'off' }}
+          isLoading={isLoading}
+          labelKey={(user: IUserHasId) => `${user.username} ${user.name} ${user.email}`}
+          options={applicableUsers} // Search result
+          onSearch={handleSearch}
+          onChange={handleChange}
+          onKeyDown={onKeyDown}
+          minLength={1}
+          searchText={isLoading ? 'Searching...' : (isSearchError && 'Error on searching.')}
+          renderMenuItemChildren={renderMenuItemChildren}
+          align="left"
+          clearButton
+        />
+      </div>
+      <div className="col-2 ps-0">
+        <button
+          type="button"
+          className="btn btn-success"
+          disabled={inputUser.length === 0}
+          onClick={addUserBySubmit}
+        >
+          {t('add')}
+        </button>
+      </div>
+    </div>
+  );
+};

+ 6 - 5
apps/app/src/components/Admin/UserGroupDetail/UserGroupUserModal.tsx

@@ -1,16 +1,17 @@
 import React from 'react';
 import React from 'react';
 
 
-import type { IUserGroupHasId } from '@growi/core';
+import type { IUserGroupHasId, IUserHasId } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import {
 import {
   Modal, ModalHeader, ModalBody,
   Modal, ModalHeader, ModalBody,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
-import { SearchTypes, SearchType } from '~/interfaces/user-group';
+import type { SearchType } from '~/interfaces/user-group';
+import { SearchTypes } from '~/interfaces/user-group';
 
 
 import CheckBoxForSerchUserOption from './CheckBoxForSerchUserOption';
 import CheckBoxForSerchUserOption from './CheckBoxForSerchUserOption';
 import RadioButtonForSerchUserOption from './RadioButtonForSerchUserOption';
 import RadioButtonForSerchUserOption from './RadioButtonForSerchUserOption';
-import UserGroupUserFormByInput from './UserGroupUserFormByInput';
+import { UserGroupUserFormByInput } from './UserGroupUserFormByInput';
 
 
 type Props = {
 type Props = {
   isOpen: boolean,
   isOpen: boolean,
@@ -19,7 +20,7 @@ type Props = {
   isAlsoMailSearched: boolean,
   isAlsoMailSearched: boolean,
   isAlsoNameSearched: boolean,
   isAlsoNameSearched: boolean,
   onClickAddUserBtn: (username: string) => Promise<void>,
   onClickAddUserBtn: (username: string) => Promise<void>,
-  onSearchApplicableUsers: (searchWord: string) => Promise<void>,
+  onSearchApplicableUsers: (searchWord: string) => Promise<IUserHasId[]>,
   onSwitchSearchType: (searchType: SearchType) => void
   onSwitchSearchType: (searchType: SearchType) => void
   onClose: () => void,
   onClose: () => void,
   onToggleIsAlsoMailSearched: () => void,
   onToggleIsAlsoMailSearched: () => void,
@@ -54,9 +55,9 @@ const UserGroupUserModal = (props: Props): JSX.Element => {
             userGroup={userGroup}
             userGroup={userGroup}
             onClickAddUserBtn={onClickAddUserBtn}
             onClickAddUserBtn={onClickAddUserBtn}
             onSearchApplicableUsers={onSearchApplicableUsers}
             onSearchApplicableUsers={onSearchApplicableUsers}
-            onClose={onClose}
             isAlsoNameSearched={isAlsoNameSearched}
             isAlsoNameSearched={isAlsoNameSearched}
             isAlsoMailSearched={isAlsoMailSearched}
             isAlsoMailSearched={isAlsoMailSearched}
+            searchType={searchType}
           />
           />
         </div>
         </div>
         <h2 className="border-bottom">{t('admin:user_group_management.add_modal.search_option')}</h2>
         <h2 className="border-bottom">{t('admin:user_group_management.add_modal.search_option')}</h2>

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

@@ -52,9 +52,9 @@ export const UserGroupUserTable = (props: Props): JSX.Element => {
                       type="button"
                       type="button"
                       id={`admin-group-menu-button-${relatedUser._id}`}
                       id={`admin-group-menu-button-${relatedUser._id}`}
                       className="btn btn-outline-secondary btn-sm dropdown-toggle"
                       className="btn btn-outline-secondary btn-sm dropdown-toggle"
-                      data-toggle="dropdown"
+                      data-bs-toggle="dropdown"
                     >
                     >
-                      <i className="icon-settings"></i>
+                      <span className="material-symbols-outlined fs-5">settings</span>
                     </button>
                     </button>
                     <div className="dropdown-menu" role="menu" aria-labelledby={`admin-group-menu-button-${relatedUser._id}`}>
                     <div className="dropdown-menu" role="menu" aria-labelledby={`admin-group-menu-button-${relatedUser._id}`}>
                       <button
                       <button
@@ -62,7 +62,7 @@ export const UserGroupUserTable = (props: Props): JSX.Element => {
                         type="button"
                         type="button"
                         onClick={() => props.onClickRemoveUserBtn(relatedUser.username)}
                         onClick={() => props.onClickRemoveUserBtn(relatedUser.username)}
                       >
                       >
-                        <i className="icon-fw icon-user-unfollow"></i> {t('admin:user_group_management.remove_from_group')}
+                        <span className="material-symbols-outlined me-1">person_remove</span>{t('admin:user_group_management.remove_from_group')}
                       </button>
                       </button>
                     </div>
                     </div>
                   </div>
                   </div>

+ 1 - 1
apps/app/src/components/Admin/UserManagement.module.scss

@@ -12,7 +12,7 @@
   }
   }
   .search-clear {
   .search-clear {
     position: absolute;
     position: absolute;
-    top: 12px;
+    top: 15px;
     right: 1px;
     right: 1px;
     z-index: 3;
     z-index: 3;
     width: 24px;
     width: 24px;

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

@@ -149,15 +149,16 @@ const UserManagement = (props: UserManagementProps) => {
               {
               {
                 adminUsersContainer.state.searchText.length > 0
                 adminUsersContainer.state.searchText.length > 0
                   ? (
                   ? (
-                    <i
-                      className="icon-close search-clear"
+                    <span
+                      className="material-symbols-outlined me-1 search-clear"
                       onClick={async() => {
                       onClick={async() => {
                         await adminUsersContainer.clearSearchText();
                         await adminUsersContainer.clearSearchText();
                         if (inputRef.current != null) {
                         if (inputRef.current != null) {
                           inputRef.current.value = '';
                           inputRef.current.value = '';
                         }
                         }
                       }}
                       }}
-                    />
+                    >cancel
+                    </span>
                   )
                   )
                   : ''
                   : ''
               }
               }

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

@@ -63,7 +63,7 @@ const ExternalAccountTable = (props: ExternalAccountTableProps): JSX.Element =>
                   data-html="true"
                   data-html="true"
                   title={t('user_management.password_setting_help')}
                   title={t('user_management.password_setting_help')}
                 >
                 >
-                  <small><i className="icon-question" aria-hidden="true"></i></small>
+                  <small><span className="material-symbols-outlined" aria-hidden="true">help</span></small>
                 </span>
                 </span>
               </div>
               </div>
             </th>
             </th>
@@ -92,7 +92,7 @@ const ExternalAccountTable = (props: ExternalAccountTableProps): JSX.Element =>
                 <td>
                 <td>
                   <div className="btn-group admin-user-menu">
                   <div className="btn-group admin-user-menu">
                     <button type="button" className="btn btn-outline-secondary btn-sm dropdown-toggle" data-bs-toggle="dropdown">
                     <button type="button" className="btn btn-outline-secondary btn-sm dropdown-toggle" data-bs-toggle="dropdown">
-                      <i className="icon-settings"></i> <span className="caret"></span>
+                      <span className="material-symbols-outlined">settings</span> <span className="caret"></span>
                     </button>
                     </button>
                     <ul className="dropdown-menu" role="menu">
                     <ul className="dropdown-menu" role="menu">
                       <li className="dropdown-header">{t('user_management.user_table.edit_menu')}</li>
                       <li className="dropdown-header">{t('user_management.user_table.edit_menu')}</li>

+ 1 - 1
apps/app/src/components/Admin/Users/GrantAdminButton.tsx

@@ -30,7 +30,7 @@ const GrantAdminButton = (props: GrantAdminButtonProps): JSX.Element => {
 
 
   return (
   return (
     <button className="dropdown-item" type="button" onClick={() => onClickGrantAdminBtnHandler()}>
     <button className="dropdown-item" type="button" onClick={() => onClickGrantAdminBtnHandler()}>
-      <i className="icon-fw icon-user-following"></i> {t('user_management.user_table.grant_admin_access')}
+      <span className="material-symbols-outlined me-1">person_add</span>{t('user_management.user_table.grant_admin_access')}
     </button>
     </button>
   );
   );
 
 

+ 1 - 1
apps/app/src/components/Admin/Users/GrantReadOnlyButton.tsx

@@ -26,7 +26,7 @@ const GrantReadOnlyButton: React.FC<{
 
 
   return (
   return (
     <button className="dropdown-item" type="button" onClick={onClickGrantReadOnlyBtnHandler}>
     <button className="dropdown-item" type="button" onClick={onClickGrantReadOnlyBtnHandler}>
-      <i className="icon-fw icon-user-following"></i> {t('user_management.user_table.grant_read_only_access')}
+      <span className="material-symbols-outlined me-1">person_add</span>{t('user_management.user_table.grant_read_only_access')}
     </button>
     </button>
   );
   );
 };
 };

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

@@ -33,7 +33,7 @@ const RevokeAdminButton = (props: RevokeAdminButtonProps): JSX.Element => {
   const renderRevokeAdminBtn = () => {
   const renderRevokeAdminBtn = () => {
     return (
     return (
       <button className="dropdown-item" type="button" onClick={() => onClickRevokeAdminBtnHandler()}>
       <button className="dropdown-item" type="button" onClick={() => onClickRevokeAdminBtnHandler()}>
-        <i className="icon-fw icon-user-unfollow"></i>{t('user_management.user_table.revoke_admin_access')}
+        <span className="material-symbols-outlined me-1">person_remove</span>{t('user_management.user_table.revoke_admin_access')}
       </button>
       </button>
     );
     );
   };
   };
@@ -41,7 +41,7 @@ const RevokeAdminButton = (props: RevokeAdminButtonProps): JSX.Element => {
   const renderRevokeAdminAlert = () => {
   const renderRevokeAdminAlert = () => {
     return (
     return (
       <div className="px-4">
       <div className="px-4">
-        <i className="icon-fw icon-user-unfollow mb-2"></i>{t('user_management.user_table.revoke_admin_access')}
+        <span className="material-symbols-outlined me-1 mb-2">person_remove</span>{t('user_management.user_table.revoke_admin_access')}
         <p className="alert alert-danger">{t('user_management.user_table.cannot_revoke')}</p>
         <p className="alert alert-danger">{t('user_management.user_table.cannot_revoke')}</p>
       </div>
       </div>
     );
     );

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

@@ -15,7 +15,7 @@ const RevokeAdminAlert = React.memo((): JSX.Element => {
 
 
   return (
   return (
     <div className="px-4">
     <div className="px-4">
-      <i className="icon-fw icon-user-unfollow mb-2"></i>{t('admin:user_management.user_table.revoke_admin_access')}
+      <span className="material-symbols-outlined me-1 mb-2">person_remove</span>{t('admin:user_management.user_table.revoke_admin_access')}
       <p className="alert alert-danger">{t('admin:user_management.user_table.cannot_revoke')}</p>
       <p className="alert alert-danger">{t('admin:user_management.user_table.cannot_revoke')}</p>
     </div>
     </div>
   );
   );
@@ -49,7 +49,7 @@ const RevokeAdminMenuItem = (props: Props): JSX.Element => {
   return user.username !== currentUser?.username
   return user.username !== currentUser?.username
     ? (
     ? (
       <button className="dropdown-item" type="button" onClick={clickRevokeAdminBtnHandler}>
       <button className="dropdown-item" type="button" onClick={clickRevokeAdminBtnHandler}>
-        <i className="icon-fw icon-user-unfollow"></i> {t('user_management.user_table.revoke_admin_access')}
+        <span className="material-symbols-outlined me-1">person_remove</span> {t('user_management.user_table.revoke_admin_access')}
       </button>
       </button>
     )
     )
     : <RevokeAdminAlert />;
     : <RevokeAdminAlert />;

+ 1 - 1
apps/app/src/components/Admin/Users/RevokeReadOnlyMenuItem.tsx

@@ -26,7 +26,7 @@ const RevokeReadOnlyMenuItem: React.FC<{
 
 
   return (
   return (
     <button className="dropdown-item" type="button" onClick={clickRevokeReadOnlyBtnHandler}>
     <button className="dropdown-item" type="button" onClick={clickRevokeReadOnlyBtnHandler}>
-      <i className="icon-fw icon-user-unfollow"></i> {t('user_management.user_table.revoke_read_only_access')}
+      <span className="material-symbols-outlined me-1">person_remove</span> {t('user_management.user_table.revoke_read_only_access')}
     </button>
     </button>
   );
   );
 };
 };

+ 1 - 1
apps/app/src/components/Admin/Users/SendInvitationEmailButton.jsx

@@ -38,7 +38,7 @@ const SendInvitationEmailButton = (props) => {
 
 
   return (
   return (
     <button className={`dropdown-item ${textColor}`} type="button" onClick={() => { onClickSendInvitationEmailButton() }}>
     <button className={`dropdown-item ${textColor}`} type="button" onClick={() => { onClickSendInvitationEmailButton() }}>
-      <i className="icon-fw icon-envelope"></i>
+      <span className="material-symbols-outlined me-1">mail</span>
       {isInvitationEmailSended && (<>{t('admin:user_management.user_table.resend_invitation_email')}</>)}
       {isInvitationEmailSended && (<>{t('admin:user_management.user_table.resend_invitation_email')}</>)}
       {!isInvitationEmailSended && (<>{t('admin:user_management.user_table.send_invitation_email')}</>)}
       {!isInvitationEmailSended && (<>{t('admin:user_management.user_table.send_invitation_email')}</>)}
     </button>
     </button>

+ 1 - 1
apps/app/src/components/Admin/Users/StatusActivateButton.jsx

@@ -33,7 +33,7 @@ class StatusActivateButton extends React.Component {
 
 
     return (
     return (
       <button className="dropdown-item" type="button" onClick={() => { this.onClickAcceptBtn() }}>
       <button className="dropdown-item" type="button" onClick={() => { this.onClickAcceptBtn() }}>
-        <i className="icon-fw icon-user-following"></i> {t('user_management.user_table.accept')}
+        <span className="material-symbols-outlined me-1">person_add</span>{t('user_management.user_table.accept')}
       </button>
       </button>
     );
     );
   }
   }

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

@@ -14,7 +14,7 @@ const SuspendAlert = React.memo((): JSX.Element => {
 
 
   return (
   return (
     <div className="px-4">
     <div className="px-4">
-      <i className="icon-fw icon-ban mb-2"></i>{t('admin:user_management.user_table.deactivate_account')}
+      <span className="material-symbols-outlined me-1 mb-2">cancel</span>{t('admin:user_management.user_table.deactivate_account')}
       <p className="alert alert-danger">{t('admin:user_management.user_table.your_own')}</p>
       <p className="alert alert-danger">{t('admin:user_management.user_table.your_own')}</p>
     </div>
     </div>
   );
   );
@@ -47,7 +47,7 @@ const StatusSuspendMenuItem = (props: Props): JSX.Element => {
   return user.username !== currentUser?.username
   return user.username !== currentUser?.username
     ? (
     ? (
       <button className="dropdown-item" type="button" onClick={clickDeactiveBtnHandler}>
       <button className="dropdown-item" type="button" onClick={clickDeactiveBtnHandler}>
-        <i className="icon-fw icon-ban"></i> {t('user_management.user_table.deactivate_account')}
+        <span className="material-symbols-outlined me-1">cancel</span> {t('user_management.user_table.deactivate_account')}
       </button>
       </button>
     )
     )
     : <SuspendAlert />;
     : <SuspendAlert />;

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

@@ -48,7 +48,7 @@ const UserMenu = (props: UserMenuProps) => {
         <li className="dropdown-header">{t('user_management.user_table.edit_menu')}</li>
         <li className="dropdown-header">{t('user_management.user_table.edit_menu')}</li>
         <li>
         <li>
           <button className="dropdown-item" type="button" onClick={onClickPasswordResetHandler}>
           <button className="dropdown-item" type="button" onClick={onClickPasswordResetHandler}>
-            <i className="icon-fw icon-key"></i>{ t('user_management.reset_password') }
+            <span className="material-symbols-outlined me-1">key</span>{ t('user_management.reset_password') }
           </button>
           </button>
         </li>
         </li>
       </>
       </>
@@ -95,7 +95,8 @@ const UserMenu = (props: UserMenuProps) => {
   return (
   return (
     <UncontrolledDropdown id="userMenu" size="sm">
     <UncontrolledDropdown id="userMenu" size="sm">
       <DropdownToggle caret color="secondary" outline>
       <DropdownToggle caret color="secondary" outline>
-        <i className="icon-settings" />
+        {/* TODO:fontsize: 20px */}
+        <span className="material-symbols-outlined fs-5">settings</span>
         {(user.status === USER_STATUS.INVITED && !isInvitationEmailSended)
         {(user.status === USER_STATUS.INVITED && !isInvitationEmailSended)
         && <i className={`fa fa-circle text-danger grw-usermenu-notification-icon ${styles['grw-usermenu-notification-icon']}`} />}
         && <i className={`fa fa-circle text-danger grw-usermenu-notification-icon ${styles['grw-usermenu-notification-icon']}`} />}
       </DropdownToggle>
       </DropdownToggle>

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

@@ -23,7 +23,7 @@ export const BookmarkFolderItemControl: React.FC<{
     <Dropdown isOpen={isOpen} toggle={() => setIsOpen(!isOpen)}>
     <Dropdown isOpen={isOpen} toggle={() => setIsOpen(!isOpen)}>
       { children ?? (
       { children ?? (
         <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control d-flex align-items-center justify-content-center">
         <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control d-flex align-items-center justify-content-center">
-          <i className="icon-options"></i>
+          <span className="material-symbols-outlined">more_horiz</span>
         </DropdownToggle>
         </DropdownToggle>
       ) }
       ) }
       <DropdownMenu
       <DropdownMenu
@@ -43,7 +43,7 @@ export const BookmarkFolderItemControl: React.FC<{
           onClick={onClickRename}
           onClick={onClickRename}
           className="grw-page-control-dropdown-item"
           className="grw-page-control-dropdown-item"
         >
         >
-          <i className="icon-fw icon-action-redo grw-page-control-dropdown-icon"></i>
+          <span className="material-symbols-outlined me-1 grw-page-control-dropdown-icon">redo</span>
           {t('Rename')}
           {t('Rename')}
         </DropdownItem>
         </DropdownItem>
 
 
@@ -53,7 +53,7 @@ export const BookmarkFolderItemControl: React.FC<{
           className="pt-2 grw-page-control-dropdown-item text-danger"
           className="pt-2 grw-page-control-dropdown-item text-danger"
           onClick={onClickDelete}
           onClick={onClickDelete}
         >
         >
-          <span className="material-symbols-outlined grw-page-control-dropdown-icon">delete</span>
+          <span className="material-symbols-outlined me-1 grw-page-control-dropdown-icon">delete</span>
           {t('Delete')}
           {t('Delete')}
         </DropdownItem>
         </DropdownItem>
       </DropdownMenu>
       </DropdownMenu>

+ 0 - 4
apps/app/src/components/Bookmarks/BookmarkFolderTree.module.scss

@@ -24,10 +24,6 @@ $grw-bookmark-item-padding-left: 35px;
 
 
 .grw-foldertree :global {
 .grw-foldertree :global {
 
 
-  .btn-page-item-control .icon-plus::before {
-    font-size: 18px;
-  }
-
   .list-group-item {
   .list-group-item {
     .grw-visible-on-hover {
     .grw-visible-on-hover {
       display: none;
       display: none;

+ 4 - 3
apps/app/src/components/Bookmarks/BookmarkItem.tsx

@@ -14,7 +14,8 @@ import { bookmark, unbookmark, unlink } from '~/client/services/page-operation';
 import { addBookmarkToFolder, renamePage } from '~/client/util/bookmark-utils';
 import { addBookmarkToFolder, renamePage } from '~/client/util/bookmark-utils';
 import { ValidationTarget } from '~/client/util/input-validator';
 import { ValidationTarget } from '~/client/util/input-validator';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { toastError, toastSuccess } from '~/client/util/toastr';
-import { BookmarkFolderItems, DragItemDataType, DRAG_ITEM_TYPE } from '~/interfaces/bookmark-info';
+import type { BookmarkFolderItems, DragItemDataType } from '~/interfaces/bookmark-info';
+import { DRAG_ITEM_TYPE } from '~/interfaces/bookmark-info';
 import { usePutBackPageModal } from '~/stores/modal';
 import { usePutBackPageModal } from '~/stores/modal';
 import { mutateAllPageInfo, useSWRMUTxCurrentPage, useSWRxPageInfo } from '~/stores/page';
 import { mutateAllPageInfo, useSWRMUTxCurrentPage, useSWRxPageInfo } from '~/stores/page';
 
 
@@ -180,7 +181,7 @@ export const BookmarkItem = (props: Props): JSX.Element => {
               : undefined}
               : undefined}
           >
           >
             <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control p-0 grw-visible-on-hover me-1">
             <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control p-0 grw-visible-on-hover me-1">
-              <i className="icon-options fa fa-rotate-90 p-1"></i>
+              <span className="material-symbols-outlined p-1">more_vert</span>
             </DropdownToggle>
             </DropdownToggle>
           </PageItemControl>
           </PageItemControl>
         </div>
         </div>
@@ -191,7 +192,7 @@ export const BookmarkItem = (props: Props): JSX.Element => {
           target={bookmarkItemId}
           target={bookmarkItemId}
           fade={false}
           fade={false}
         >
         >
-          {formerPagePath !== null ? `${formerPagePath}/` : '/'}
+          {dPagePath.isFormerRoot ? '/' : `${formerPagePath}/`}
         </UncontrolledTooltip>
         </UncontrolledTooltip>
       </li>
       </li>
     </DragAndDropWrapper>
     </DragAndDropWrapper>

+ 25 - 18
apps/app/src/components/Common/ClosableTextInput.tsx

@@ -4,6 +4,7 @@ import React, {
 } from 'react';
 } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
+import AutosizeInput from 'react-input-autosize';
 
 
 import type { AlertInfo } from '~/client/util/input-validator';
 import type { AlertInfo } from '~/client/util/input-validator';
 import { AlertType, inputValidator } from '~/client/util/input-validator';
 import { AlertType, inputValidator } from '~/client/util/input-validator';
@@ -12,10 +13,12 @@ type ClosableTextInputProps = {
   value?: string
   value?: string
   placeholder?: string
   placeholder?: string
   validationTarget?: string,
   validationTarget?: string,
+  useAutosizeInput?: boolean
+  inputClassName?: string,
   onPressEnter?(inputText: string | null): void
   onPressEnter?(inputText: string | null): void
   onPressEscape?: () => void
   onPressEscape?: () => void
   onClickOutside?(): void
   onClickOutside?(): void
-  handleInputChange?: (string) => void
+  onChange?(inputText: string): void
 }
 }
 
 
 const ClosableTextInput: FC<ClosableTextInputProps> = memo((props: ClosableTextInputProps) => {
 const ClosableTextInput: FC<ClosableTextInputProps> = memo((props: ClosableTextInputProps) => {
@@ -43,7 +46,7 @@ const ClosableTextInput: FC<ClosableTextInputProps> = memo((props: ClosableTextI
     setInputText(inputText);
     setInputText(inputText);
     setIsAbleToShowAlert(true);
     setIsAbleToShowAlert(true);
 
 
-    props.handleInputChange?.(inputText);
+    props.onChange?.(inputText);
   };
   };
 
 
   const onFocusHandler = async(e: React.ChangeEvent<HTMLInputElement>) => {
   const onFocusHandler = async(e: React.ChangeEvent<HTMLInputElement>) => {
@@ -115,25 +118,29 @@ const ClosableTextInput: FC<ClosableTextInputProps> = memo((props: ClosableTextI
     );
     );
   };
   };
 
 
+  const inputProps = {
+    'data-testid': 'closable-text-input',
+    value: inputText || '',
+    ref: inputRef,
+    type: 'text',
+    placeholder: props.placeholder,
+    name: 'input',
+    onFocus: onFocusHandler,
+    onChange: onChangeHandler,
+    onKeyDown: onKeyDownHandler,
+    onCompositionStart: () => setComposing(true),
+    onCompositionEnd: () => setComposing(false),
+    onBlur: onBlurHandler,
+  };
+
+  const inputClassName = `form-control ${props.inputClassName ?? ''}`;
 
 
   return (
   return (
     <div>
     <div>
-      <input
-        value={inputText || ''}
-        ref={inputRef}
-        type="text"
-        className="form-control"
-        placeholder={props.placeholder}
-        name="input"
-        data-testid="closable-text-input"
-        onFocus={onFocusHandler}
-        onChange={onChangeHandler}
-        onKeyDown={onKeyDownHandler}
-        onCompositionStart={() => setComposing(true)}
-        onCompositionEnd={() => setComposing(false)}
-        onBlur={onBlurHandler}
-        autoFocus={false}
-      />
+      { props.useAutosizeInput
+        ? <AutosizeInput inputClassName={inputClassName} {...inputProps} />
+        : <input className={inputClassName} {...inputProps} />
+      }
       {isAbleToShowAlert && <AlertInfo />}
       {isAbleToShowAlert && <AlertInfo />}
     </div>
     </div>
   );
   );

+ 2 - 1
apps/app/src/components/Common/CountBadge.tsx

@@ -1,4 +1,5 @@
-import React, { FC } from 'react';
+import type { FC } from 'react';
+import React from 'react';
 
 
 type CountProps = {
 type CountProps = {
   count?: number,
   count?: number,

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

@@ -11,7 +11,7 @@ import {
 } from 'reactstrap';
 } from 'reactstrap';
 
 
 import { NotAvailableForGuest } from '~/components/NotAvailableForGuest';
 import { NotAvailableForGuest } from '~/components/NotAvailableForGuest';
-import { IPageOperationProcessData } from '~/interfaces/page-operation';
+import type { IPageOperationProcessData } from '~/interfaces/page-operation';
 import { useSWRxPageInfo } from '~/stores/page';
 import { useSWRxPageInfo } from '~/stores/page';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { shouldRecoverPagePaths } from '~/utils/page-operation';
 import { shouldRecoverPagePaths } from '~/utils/page-operation';
@@ -182,7 +182,7 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
             data-testid="open-page-move-rename-modal-btn"
             data-testid="open-page-move-rename-modal-btn"
             className="grw-page-control-dropdown-item"
             className="grw-page-control-dropdown-item"
           >
           >
-            <i className="icon-fw icon-action-redo grw-page-control-dropdown-icon"></i>
+            <span className="material-symbols-outlined me-1 grw-page-control-dropdown-icon">redo</span>
             {t(isInstantRename ? 'Rename' : 'Move/Rename')}
             {t(isInstantRename ? 'Rename' : 'Move/Rename')}
           </DropdownItem>
           </DropdownItem>
         ) }
         ) }
@@ -194,7 +194,7 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
             data-testid="open-page-duplicate-modal-btn"
             data-testid="open-page-duplicate-modal-btn"
             className="grw-page-control-dropdown-item"
             className="grw-page-control-dropdown-item"
           >
           >
-            <i className="icon-fw icon-docs grw-page-control-dropdown-icon"></i>
+            <span className="material-symbols-outlined me-1 grw-page-control-dropdown-icon">file_copy</span>
             {t('Duplicate')}
             {t('Duplicate')}
           </DropdownItem>
           </DropdownItem>
         ) }
         ) }
@@ -205,7 +205,7 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
             onClick={revertItemClickedHandler}
             onClick={revertItemClickedHandler}
             className="grw-page-control-dropdown-item"
             className="grw-page-control-dropdown-item"
           >
           >
-            <i className="icon-fw icon-action-undo grw-page-control-dropdown-icon"></i>
+            <span className="material-symbols-outlined me-1 grw-page-control-dropdown-icon">undo</span>
             {t('modal_putback.label.Put Back Page')}
             {t('modal_putback.label.Put Back Page')}
           </DropdownItem>
           </DropdownItem>
         ) }
         ) }
@@ -223,7 +223,7 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
             onClick={pathRecoveryItemClickedHandler}
             onClick={pathRecoveryItemClickedHandler}
             className="grw-page-control-dropdown-item"
             className="grw-page-control-dropdown-item"
           >
           >
-            <i className="icon-fw icon-wrench grw-page-control-dropdown-icon"></i>
+            <span className="material-symbols-outlined me-1 grw-page-control-dropdown-icon">build</span>
             {t('PathRecovery')}
             {t('PathRecovery')}
           </DropdownItem>
           </DropdownItem>
         ) }
         ) }
@@ -239,7 +239,7 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
               onClick={deleteItemClickedHandler}
               onClick={deleteItemClickedHandler}
               data-testid="open-page-delete-modal-btn"
               data-testid="open-page-delete-modal-btn"
             >
             >
-              <i className="icon-fw icon-trash grw-page-control-dropdown-icon"></i>
+              <span className="material-symbols-outlined me-1 grw-page-control-dropdown-icon">delete</span>
               {t('Delete')}
               {t('Delete')}
             </DropdownItem>
             </DropdownItem>
           </>
           </>

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

@@ -3,7 +3,7 @@ import React, { memo, useCallback } from 'react';
 import Link from 'next/link';
 import Link from 'next/link';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 
 
-import LinkedPagePath from '../../../models/linked-page-path';
+import type LinkedPagePath from '../../../models/linked-page-path';
 
 
 import styles from './PagePathHierarchicalLink.module.scss';
 import styles from './PagePathHierarchicalLink.module.scss';
 
 
@@ -51,7 +51,8 @@ export const PagePathHierarchicalLink = memo((props: PagePathHierarchicalLinkPro
         <RootElm>
         <RootElm>
           <span className="path-segment">
           <span className="path-segment">
             <Link href="/" prefetch={false}>
             <Link href="/" prefetch={false}>
-              <i className="icon-home"></i>
+              {/* TODO: Size adjust */}
+              <span className="material-symbols-outlined">home</span>
               <span className={`separator ${styles.separator}`}>/</span>
               <span className={`separator ${styles.separator}`}>/</span>
             </Link>
             </Link>
           </span>
           </span>

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

@@ -6,6 +6,11 @@
   margin-right: 0.2em;
   margin-right: 0.2em;
 }
 }
 
 
+.grw-mx-02em {
+  margin-right: 0.2em;
+  margin-left: 0.2em;
+}
+
 .grw-page-path-nav-sticky :global {
 .grw-page-path-nav-sticky :global {
   min-height: 75px;
   min-height: 75px;
 
 

+ 19 - 6
apps/app/src/components/Common/PagePathNav/PagePathNav.tsx

@@ -1,4 +1,5 @@
-import React, { FC } from 'react';
+import type { FC } from 'react';
+import React from 'react';
 
 
 import { DevidedPagePath } from '@growi/core/dist/models';
 import { DevidedPagePath } from '@growi/core/dist/models';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { pagePathUtils } from '@growi/core/dist/utils';
@@ -19,6 +20,7 @@ const { isTrashPage } = pagePathUtils;
 type Props = {
 type Props = {
   pagePath: string,
   pagePath: string,
   pageId?: string | null,
   pageId?: string | null,
+  isWipPage?: boolean,
   isSingleLineMode?: boolean,
   isSingleLineMode?: boolean,
   isCollapseParents?: boolean,
   isCollapseParents?: boolean,
   formerLinkClassName?: string,
   formerLinkClassName?: string,
@@ -27,13 +29,16 @@ type Props = {
 
 
 const CopyDropdown = dynamic(() => import('../CopyDropdown').then(mod => mod.CopyDropdown), { ssr: false });
 const CopyDropdown = dynamic(() => import('../CopyDropdown').then(mod => mod.CopyDropdown), { ssr: false });
 
 
-const Separator = (): JSX.Element => {
+const RootSlash = (): JSX.Element => {
   return <span className={styles['grw-mr-02em']}>/</span>;
   return <span className={styles['grw-mr-02em']}>/</span>;
 };
 };
+const Separator = (): JSX.Element => {
+  return <span className={styles['grw-mx-02em']}>/</span>;
+};
 
 
 export const PagePathNav: FC<Props> = (props: Props) => {
 export const PagePathNav: FC<Props> = (props: Props) => {
   const {
   const {
-    pageId, pagePath, isSingleLineMode, isCollapseParents,
+    pageId, pagePath, isWipPage, isSingleLineMode, isCollapseParents,
     formerLinkClassName, latterLinkClassName,
     formerLinkClassName, latterLinkClassName,
   } = props;
   } = props;
   const dPagePath = new DevidedPagePath(pagePath, false, true);
   const dPagePath = new DevidedPagePath(pagePath, false, true);
@@ -66,10 +71,15 @@ export const PagePathNav: FC<Props> = (props: Props) => {
   else {
   else {
     const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
     const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
     const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
     const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
-    formerLink = <PagePathHierarchicalLink linkedPagePath={linkedPagePathFormer} isInTrash={isInTrash} />;
-    latterLink = (
+    formerLink = (
       <>
       <>
+        <PagePathHierarchicalLink linkedPagePath={linkedPagePathFormer} isInTrash={isInTrash} />
         <Separator />
         <Separator />
+      </>
+    );
+    latterLink = (
+      <>
+        <RootSlash />
         <PagePathHierarchicalLink linkedPagePath={linkedPagePathLatter} basePath={dPagePath.former} isInTrash={isInTrash} />
         <PagePathHierarchicalLink linkedPagePath={linkedPagePathLatter} basePath={dPagePath.former} isInTrash={isInTrash} />
       </>
       </>
     );
     );
@@ -85,7 +95,10 @@ export const PagePathNav: FC<Props> = (props: Props) => {
           {latterLink}
           {latterLink}
         </h1>
         </h1>
         { pageId != null && !isNotFound && (
         { pageId != null && !isNotFound && (
-          <div className="mx-2">
+          <div className="d-flex align-items-center ms-2">
+            { isWipPage && (
+              <span className="badge rounded-pill text-bg-secondary ms-1 me-1">WIP</span>
+            )}
             <CopyDropdown pageId={pageId} pagePath={pagePath} dropdownToggleId={copyDropdownId} dropdownToggleClassName="p-2">
             <CopyDropdown pageId={pageId} pagePath={pagePath} dropdownToggleId={copyDropdownId} dropdownToggleClassName="p-2">
               <i className="ti ti-clipboard"></i>
               <i className="ti ti-clipboard"></i>
             </CopyDropdown>
             </CopyDropdown>

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

@@ -1,6 +0,0 @@
-.grw-icon-container-recently-created :global {
-  svg {
-    width: 14px;
-    height: 14px;
-  }
-}

+ 13 - 18
apps/app/src/components/ContentLinkButtons.tsx

@@ -1,22 +1,19 @@
 import React from 'react';
 import React from 'react';
 
 
-import type { IUserHasId } from '@growi/core';
+import { USER_STATUS, type IUserHasId } from '@growi/core';
+import { useTranslation } from 'next-i18next';
 import { Link as ScrollLink } from 'react-scroll';
 import { Link as ScrollLink } from 'react-scroll';
 
 
-import { RecentlyCreatedIcon } from '~/components/Icons/RecentlyCreatedIcon';
-
-import styles from './ContentLinkButtons.module.scss';
-
 const BookMarkLinkButton = React.memo(() => {
 const BookMarkLinkButton = React.memo(() => {
-
+  const { t } = useTranslation();
   return (
   return (
     <ScrollLink to="bookmarks-list" offset={-120}>
     <ScrollLink to="bookmarks-list" offset={-120}>
       <button
       <button
         type="button"
         type="button"
-        className="btn btn-outline-secondary btn-sm px-2"
+        className="btn btn-sm btn-outline-neutral-secondary rounded-pill d-flex align-items-center w-100"
       >
       >
-        <span className="material-symbols-outlined">bookmark</span>
-        <span>Bookmarks</span>
+        <span className="material-symbols-outlined p-0">bookmark</span>
+        <span>{t('footer.bookmarks')}</span>
       </button>
       </button>
     </ScrollLink>
     </ScrollLink>
   );
   );
@@ -25,15 +22,15 @@ const BookMarkLinkButton = React.memo(() => {
 BookMarkLinkButton.displayName = 'BookMarkLinkButton';
 BookMarkLinkButton.displayName = 'BookMarkLinkButton';
 
 
 const RecentlyCreatedLinkButton = React.memo(() => {
 const RecentlyCreatedLinkButton = React.memo(() => {
-
+  const { t } = useTranslation();
   return (
   return (
     <ScrollLink to="recently-created-list" offset={-120}>
     <ScrollLink to="recently-created-list" offset={-120}>
       <button
       <button
         type="button"
         type="button"
-        className="btn btn-outline-secondary btn-sm px-3"
+        className="btn btn-sm btn-outline-neutral-secondary rounded-pill d-flex align-items-center w-100"
       >
       >
-        <i className={`${styles['grw-icon-container-recently-created']} grw-icon-container-recently-created me-2`}><RecentlyCreatedIcon /></i>
-        <span>Recently Created</span>
+        <span className="growi-custom-icons mx-1">recently_created</span>
+        <span>{t('footer.recently_created')}</span>
       </button>
       </button>
     </ScrollLink>
     </ScrollLink>
   );
   );
@@ -43,22 +40,20 @@ RecentlyCreatedLinkButton.displayName = 'RecentlyCreatedLinkButton';
 
 
 
 
 export type ContentLinkButtonsProps = {
 export type ContentLinkButtonsProps = {
-  author?: IUserHasId,
+  author: IUserHasId | null,
 }
 }
 
 
 export const ContentLinkButtons = (props: ContentLinkButtonsProps): JSX.Element => {
 export const ContentLinkButtons = (props: ContentLinkButtonsProps): JSX.Element => {
-
   const { author } = props;
   const { author } = props;
 
 
-  if (author == null || author.status === 4) {
+  if (author == null || author.status === USER_STATUS.DELETED) {
     return <></>;
     return <></>;
   }
   }
 
 
   return (
   return (
-    <div className="mt-3 d-flex justify-content-between">
+    <div className="d-grid gap-2">
       <BookMarkLinkButton />
       <BookMarkLinkButton />
       <RecentlyCreatedLinkButton />
       <RecentlyCreatedLinkButton />
     </div>
     </div>
   );
   );
-
 };
 };

+ 13 - 8
apps/app/src/components/CreateTemplateModal.tsx

@@ -4,9 +4,9 @@ import { pathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { Modal, ModalHeader, ModalBody } from 'reactstrap';
 import { Modal, ModalHeader, ModalBody } from 'reactstrap';
 
 
-import { useOnTemplateButtonClicked } from '~/client/services/use-on-template-button-clicked';
+import { useCreateTemplatePage } from '~/client/services/create-page';
 import { toastError } from '~/client/util/toastr';
 import { toastError } from '~/client/util/toastr';
-import { TargetType, LabelType } from '~/interfaces/template';
+import type { TargetType, LabelType } from '~/interfaces/template';
 
 
 
 
 type TemplateCardProps = {
 type TemplateCardProps = {
@@ -53,18 +53,19 @@ type CreateTemplateModalProps = {
 export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
 export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
   path, isOpen, onClose,
   path, isOpen, onClose,
 }) => {
 }) => {
-  const { t } = useTranslation();
+  const { t } = useTranslation(['translation', 'commons']);
 
 
-  const { onClickHandler: onClickTemplateButton, isPageCreating } = useOnTemplateButtonClicked(path);
+  const { createTemplate, isCreating, isCreatable } = useCreateTemplatePage();
 
 
   const onClickTemplateButtonHandler = useCallback(async(label: LabelType) => {
   const onClickTemplateButtonHandler = useCallback(async(label: LabelType) => {
     try {
     try {
-      await onClickTemplateButton(label);
+      await createTemplate?.(label);
+      onClose();
     }
     }
     catch (err) {
     catch (err) {
-      toastError(err);
+      toastError(t('toaster.create_failed', { target: path }));
     }
     }
-  }, [onClickTemplateButton]);
+  }, [createTemplate, path, t]);
 
 
   const parentPath = pathUtils.addTrailingSlash(path);
   const parentPath = pathUtils.addTrailingSlash(path);
 
 
@@ -73,12 +74,16 @@ export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
       <TemplateCard
       <TemplateCard
         target={target}
         target={target}
         label={label}
         label={label}
-        isPageCreating={isPageCreating}
+        isPageCreating={isCreating}
         onClickHandler={() => onClickTemplateButtonHandler(label)}
         onClickHandler={() => onClickTemplateButtonHandler(label)}
       />
       />
     </div>
     </div>
   );
   );
 
 
+  if (!isCreatable) {
+    return <></>;
+  }
+
   return (
   return (
     <Modal isOpen={isOpen} toggle={onClose} data-testid="page-template-modal">
     <Modal isOpen={isOpen} toggle={onClose} data-testid="page-template-modal">
       <ModalHeader tag="h4" toggle={onClose} className="bg-primary text-light">
       <ModalHeader tag="h4" toggle={onClose} className="bg-primary text-light">

+ 5 - 12
apps/app/src/components/CustomNavigation/CustomNav.module.scss

@@ -1,15 +1,3 @@
-.grw-custom-nav-tab,
-.grw-custom-nav-dropdown {
-  :global {
-    svg {
-      width: 17px;
-      height: 17px;
-      margin-right: 5px;
-      vertical-align: text-bottom;
-    }
-  }
-}
-
 .grw-custom-nav-tab :global {
 .grw-custom-nav-tab :global {
   .nav-title {
   .nav-title {
     flex-wrap: nowrap;
     flex-wrap: nowrap;
@@ -24,4 +12,9 @@
     border-bottom: 3px solid;
     border-bottom: 3px solid;
     transition: 0.3s ease-in-out;
     transition: 0.3s ease-in-out;
   }
   }
+
+  .material-symbols-outlined {
+    margin-right: 6px;
+    font-size: 18px;
+  }
 }
 }

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

@@ -2,12 +2,12 @@ import React, {
   useEffect, useState, useRef, useMemo, useCallback,
   useEffect, useState, useRef, useMemo, useCallback,
 } from 'react';
 } from 'react';
 
 
-import { Breakpoint } from '@growi/ui/dist/interfaces';
+import type { Breakpoint } from '@growi/ui/dist/interfaces';
 import {
 import {
   Nav, NavItem, NavLink,
   Nav, NavItem, NavLink,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
-import { ICustomNavTabMappings } from '~/interfaces/ui';
+import type { ICustomNavTabMappings } from '~/interfaces/ui';
 
 
 import styles from './CustomNav.module.scss';
 import styles from './CustomNav.module.scss';
 
 
@@ -49,7 +49,7 @@ export const CustomNavDropdown = (props: CustomNavDropdownProps): JSX.Element =>
   }, [onNavSelected]);
   }, [onNavSelected]);
 
 
   return (
   return (
-    <div className="grw-custom-nav-dropdown btn-group">
+    <div className="btn-group">
       <button
       <button
         className="btn btn-outline-primary btn-lg dropdown-toggle text-end"
         className="btn btn-outline-primary btn-lg dropdown-toggle text-end"
         type="button"
         type="button"

+ 3 - 5
apps/app/src/components/DescendantsPageListModal.tsx

@@ -13,10 +13,8 @@ import { useDescendantsPageListModal } from '~/stores/modal';
 
 
 import { CustomNavTab } from './CustomNavigation/CustomNav';
 import { CustomNavTab } from './CustomNavigation/CustomNav';
 import CustomTabContent from './CustomNavigation/CustomTabContent';
 import CustomTabContent from './CustomNavigation/CustomTabContent';
-import { DescendantsPageListProps } from './DescendantsPageList';
+import type { DescendantsPageListProps } from './DescendantsPageList';
 import ExpandOrContractButton from './ExpandOrContractButton';
 import ExpandOrContractButton from './ExpandOrContractButton';
-import PageListIcon from './Icons/PageListIcon';
-import TimeLineIcon from './Icons/TimeLineIcon';
 
 
 import styles from './DescendantsPageListModal.module.scss';
 import styles from './DescendantsPageListModal.module.scss';
 
 
@@ -46,7 +44,7 @@ export const DescendantsPageListModal = (): JSX.Element => {
   const navTabMapping = useMemo(() => {
   const navTabMapping = useMemo(() => {
     return {
     return {
       pagelist: {
       pagelist: {
-        Icon: PageListIcon,
+        Icon: () => <span className="material-symbols-outlined">subject</span>,
         Content: () => {
         Content: () => {
           if (status == null || status.path == null || !status.isOpened) {
           if (status == null || status.path == null || !status.isOpened) {
             return <></>;
             return <></>;
@@ -57,7 +55,7 @@ export const DescendantsPageListModal = (): JSX.Element => {
         isLinkEnabled: () => !isSharedUser,
         isLinkEnabled: () => !isSharedUser,
       },
       },
       timeline: {
       timeline: {
-        Icon: TimeLineIcon,
+        Icon: () => <span className="material-symbols-outlined">timeline</span>,
         Content: () => {
         Content: () => {
           if (status == null || !status.isOpened) {
           if (status == null || !status.isOpened) {
             return <></>;
             return <></>;

+ 4 - 2
apps/app/src/components/ExpandOrContractButton.tsx

@@ -1,4 +1,5 @@
-import React, { FC } from 'react';
+import type { FC } from 'react';
+import React from 'react';
 
 
 type Props = {
 type Props = {
   isWindowExpanded: boolean,
   isWindowExpanded: boolean,
@@ -24,9 +25,10 @@ const ExpandOrContractButton: FC<Props> = (props: Props) => {
   return (
   return (
     <button
     <button
       type="button"
       type="button"
-      className={`btn ${isWindowExpanded ? 'icon-size-actual' : 'icon-size-fullscreen'}`}
+      className="btn material-symbols-outlined"
       onClick={isWindowExpanded ? clickContractButtonHandler : clickExpandButtonHandler}
       onClick={isWindowExpanded ? clickContractButtonHandler : clickExpandButtonHandler}
     >
     >
+      {isWindowExpanded ? 'close_fullscreen' : 'open_in_full'}
     </button>
     </button>
   );
   );
 };
 };

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

@@ -1,28 +0,0 @@
-import React from 'react';
-
-const Attachment = () => (
-  <svg
-    xmlns="http://www.w3.org/2000/svg"
-    viewBox="0 0 14 14"
-    width="14px"
-    height="14px"
-  >
-    <rect width="14" height="14" fillOpacity="0" />
-    <g className="cls-1">
-      <path
-        d="M2.9,13a2,2,0,0,1-1.44-.63,2.28,2.28,0,0,1,0-3.23l7-7.38a2.48,2.48,0,0,1,1.22-.7,2.61,
-        2.61,0,0,1,1.41.09A3.46,3.46,0,0,1,12.37,2a3.94,3.94,0,0,1,.36.45A2.61,2.61,0,0,1,13,3a3.41,3.41,
-        0,0,1,.16.57,3.06,3.06,0,0,1-.82,2.75L7.07,11.86a.35.35,0,0,1-.26.13.4.4,0,0,1-.28-.1.47.47,0,0,
-        1-.12-.27.39.39,0,0,1,.11-.29l5.26-5.59a2.28,2.28,0,0,0,.65-1.62,2.07,2.07,0,0,0-.62-1.58A2.62,2.62,
-        0,0,0,11,1.93a2,2,0,0,0-1-.13,1.63,1.63,0,0,0-1,.5L2,9.67a1.52,1.52,0,0,0,0,2.16,1.28,1.28,0,0,0,
-        .44.3,1,1,0,0,0,.51.08,1.43,1.43,0,0,0,1-.49L9.49,5.84l.12-.13.11-.15a1.24,1.24,0,0,0,.1-.2,1.94,
-        1.94,0,0,0,0-.2.6.6,0,0,0,0-.22.66.66,0,0,0-.14-.2.57.57,0,0,0-.45-.22,1,1,0,0,0-.52.3L4.56,
-        9.25a.42.42,0,0,1-.17.1.34.34,0,0,1-.2,0A.4.4,0,0,1,4,9.26.34.34,0,0,1,3.89,9,.41.41,0,0,1,4,8.72L8.16,
-        4.28a1.7,1.7,0,0,1,1-.53,1.32,1.32,0,0,1,1.06.43,1.23,1.23,0,0,1,.4,1.05,1.8,1.8,0,0,1-.58,1.14L4.52,
-        12.26A2.3,2.3,0,0,1,3,13H2.9Z"
-      />
-    </g>
-  </svg>
-);
-
-export default Attachment;

+ 0 - 22
apps/app/src/components/Icons/HistoryIcon.jsx

@@ -1,22 +0,0 @@
-import React from 'react';
-
-const RecentChanges = () => (
-  <svg
-    xmlns="http://www.w3.org/2000/svg"
-    viewBox="0 0 14 14"
-    width="14px"
-    height="14px"
-  >
-    <rect width="14" height="14" fillOpacity="0" />
-    <path
-      d="M7.94.94A6.13,6.13,0,0,0,1.89,7v.1L.67,5.89a.38.38,0,0,0-.55,0,.39.39,0,0,0,0,.56L2.36,8.69,4.6,6.45a.4.4,0,0,0,0-.56.39.39,0,0,0-.56,
-      0L2.68,7.25V7A5.33,5.33,0,0,1,7.94,1.73,5.33,5.33,0,0,1,13.21,7a5.34,5.34,0,0,1-5.27,5.27H7.86A5,5,0,0,1,4,10.38a.4.4,0,0,0-.55-.07.4.4,0,
-      0,0-.07.56,5.83,5.83,0,0,0,4.52,2.19H8A6.13,6.13,0,0,0,14,7,6.13,6.13,0,0,0,7.94.94Z"
-    />
-    <path
-      d="M7.94,2.83a.4.4,0,0,0-.39.4V7.37L10,8.92a.37.37,0,0,0,.21.06.4.4,0,0,0,.21-.73L8.34,6.93V3.23A.4.4,0,0,0,7.94,2.83Z"
-    />
-  </svg>
-);
-
-export default RecentChanges;

+ 0 - 17
apps/app/src/components/Icons/PageListIcon.jsx

@@ -1,17 +0,0 @@
-import React from 'react';
-
-const PageList = () => (
-  <svg
-    xmlns="http://www.w3.org/2000/svg"
-    viewBox="0 0 14 14"
-
-  >
-    <rect width="14" height="14" fillOpacity="0" />
-    <path d="M12.63,2.72H1.37a.54.54,0,0,1,0-1.08H12.63a.54.54,0,0,1,0,1.08Z" />
-    <path d="M11.82,5.94H1.37a.55.55,0,0,1,0-1.09H11.82a.55.55,0,1,1,0,1.09Z" />
-    <path d="M9.41,9.15h-8a.54.54,0,0,1,0-1.08h8a.54.54,0,0,1,0,1.08Z" />
-    <path d="M10.84,12.36H1.37a.54.54,0,1,1,0-1.08h9.47a.54.54,0,1,1,0,1.08Z" />
-  </svg>
-);
-
-export default PageList;

+ 0 - 22
apps/app/src/components/Icons/PresentationIcon.jsx

@@ -1,22 +0,0 @@
-import React from 'react';
-
-const PresentationIcon = () => (
-  <svg
-    xmlns="http://www.w3.org/2000/svg"
-    width="14"
-    height="14"
-    viewBox="0 0 12.25 14"
-  >
-    <path
-      d="M44.261,0H32.909a.448.448,0,0,0-.449.448V7.635a.449.449,0,0,0,.9,0V.9H43.812V7.635a.449.449,0,0,0,.9,0V.448A.448.448,0,0,0,44.261,0Z"
-      transform="translate(-32.46)"
-    />
-    <path
-      d="M90.959,287.182H82.315a.448.448,0,1,0,0,.9h3.873v1.115l-3.207,3.381a.449.449,0,0,0,.652.616l2.555-2.694v2.013a.449.449,0,0,0,.9,0V
-        290.5l2.555,2.694a.449.449,0,0,0,.652-.616l-3.208-3.382v-1.114h3.873a.448.448,0,1,0,0-.9Z"
-      transform="translate(-80.512 -279.329)"
-    />
-  </svg>
-);
-
-export default PresentationIcon;

+ 0 - 35
apps/app/src/components/Icons/ShareLinkIcon.jsx

@@ -1,35 +0,0 @@
-import React from 'react';
-
-const ShareLink = () => (
-  <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 20 20">
-    <g transform="translate(-142 -502)">
-      <rect width="20" height="20" transform="translate(142 502)" fill="none" />
-      <g transform="translate(16 286.938)">
-        <path
-          d="M-1.813-3.563a2.711,2.711,0,0,0-1.274.308,2.8,2.8,0,0,0-.976.835L-11.48-6.2a2.676,2.676,
-          0,0,0,.105-.738,2.555,2.555,0,0,0-.044-.466,3.34,3.34,0,0,0-.114-.448l7.453-3.621a2.71,2.71,
-          0,0,0,.984.853,2.764,2.764,0,0,0,1.283.308,2.708,2.708,0,0,0,1.986-.826A2.708,2.708,0,0,
-          0,1-13.125a2.751,2.751,0,0,0-.378-1.406A2.793,2.793,0,0,0-.406-15.56a2.751,2.751,0,0,
-          0-1.406-.378,2.751,2.751,0,0,0-1.406.378,2.793,2.793,0,0,0-1.028,1.028,2.751,2.751,0,0,0-.378,
-          1.406v.105a.64.64,0,0,0,.009.105.641.641,0,0,1,.009.105A.641.641,0,0,0-4.6-12.7a.694.694,0,0,0,
-          .026.105.332.332,0,0,1,.018.105l-7.559,3.674a2.735,2.735,0,0,0-.923-.686,2.727,2.727,0,0,
-          0-1.151-.246,2.708,2.708,0,0,0-1.986.826A2.708,2.708,0,0,0-17-6.937a2.708,2.708,0,0,0,
-          .826,1.986,2.708,2.708,0,0,0,1.986.826A2.666,2.666,0,0,0-11.99-5.2l7.453,3.8a1.388,1.388,0,0,
-          0-.053.211q-.018.105-.026.22t-.009.22A2.751,2.751,0,0,0-4.247.656,2.792,2.792,0,0,0-3.219,
-          1.685a2.751,2.751,0,0,0,1.406.378A2.708,2.708,0,0,0,.174,1.236,2.708,2.708,0,0,0,1-.75,2.708,
-          2.708,0,0,0,.174-2.736,2.708,2.708,0,0,0-1.813-3.563Zm-1.2-10.758a1.627,1.627,0,0,1,1.2-.492,
-          1.627,1.627,0,0,1,1.2.492,1.627,1.627,0,0,1,.492,1.2,1.627,1.627,0,0,1-.492,1.2,1.627,1.627,
-          0,0,1-1.2.492,1.627,1.627,0,0,1-1.2-.492,1.627,1.627,0,0,1-.492-1.2A1.627,1.627,0,0,
-          1-3.008-14.32Zm-9.984,8.578a1.627,1.627,0,0,1-1.2.492,1.627,1.627,0,0,1-1.2-.492,1.627,
-          1.627,0,0,1-.492-1.2,1.627,1.627,0,0,1,.492-1.2,1.627,1.627,0,0,1,1.2-.492,1.627,1.627,
-          0,0,1,1.2.492,1.627,1.627,0,0,1,.492,1.2A1.627,1.627,0,0,1-12.992-5.742ZM-.617.445a1.627,
-          1.627,0,0,1-1.2.492,1.627,1.627,0,0,1-1.2-.492A1.627,1.627,0,0,1-3.5-.75a1.627,1.627,0,0,
-          1,.492-1.2,1.627,1.627,0,0,1,1.2-.492,1.627,1.627,0,0,1,1.2.492A1.627,1.627,0,0,1-.125-.75,1.627,1.627,0,0,1-.617.445Z"
-          transform="translate(144 232)"
-        />
-      </g>
-    </g>
-  </svg>
-);
-
-export default ShareLink;

+ 0 - 19
apps/app/src/components/Icons/TimeLineIcon.jsx

@@ -1,19 +0,0 @@
-import React from 'react';
-
-const TimeLine = () => (
-  <svg
-    xmlns="http://www.w3.org/2000/svg"
-    viewBox="0 0 14 14"
-
-  >
-    <rect width="14" height="14" fillOpacity="0" />
-    <path
-      d="M13.6,4.6a1.2,1.2,0,0,1-1.2,1.2,1,1,0,0,1-.3,0L10,7.89a1.1,1.1,0,0,1,0,.31,1.2,1.2,0,1,1-2.4,0,1.1,1.1,0,0,1,
-      0-.31L6.11,6.36a1.3,1.3,0,0,1-.62,0L2.75,9.1a1,1,0,0,1,0,.3A1.2,1.2,0,1,1,1.6,8.2a1,1,0,0,1,.3,0L4.64,
-      5.51a1.1,1.1,0,0,1,0-.31A1.2,1.2,0,0,1,7,5.2a1.1,1.1,0,0,1,0,.31L8.49,7a1.3,1.3,0,0,1,.62,0L11.25,4.9a1,
-      1,0,0,1-.05-.3,1.2,1.2,0,1,1,2.4,0Z"
-    />
-  </svg>
-);
-
-export default TimeLine;

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

@@ -17,11 +17,6 @@ $grw-pagetree-item-container-height: 40px;
   }
   }
 
 
   :global {
   :global {
-    .btn-page-item-control {
-      .icon-plus::before {
-        font-size: 18px;
-      }
-    }
 
 
     .list-group-item {
     .list-group-item {
       .grw-visible-on-hover {
       .grw-visible-on-hover {

+ 14 - 8
apps/app/src/components/ItemsTree/ItemsTree.tsx

@@ -11,12 +11,13 @@ import { useRouter } from 'next/router';
 import { debounce } from 'throttle-debounce';
 import { debounce } from 'throttle-debounce';
 
 
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { toastError, toastSuccess } from '~/client/util/toastr';
-import { AncestorsChildrenResult, RootPageResult, TargetAndAncestors } from '~/interfaces/page-listing-results';
-import { OnDuplicatedFunction, OnDeletedFunction } from '~/interfaces/ui';
-import { SocketEventName, UpdateDescCountData, UpdateDescCountRawData } from '~/interfaces/websocket';
-import {
-  IPageForPageDuplicateModal, usePageDuplicateModal, usePageDeleteModal,
-} from '~/stores/modal';
+import type { IPageForItem } from '~/interfaces/page';
+import type { AncestorsChildrenResult, RootPageResult, TargetAndAncestors } from '~/interfaces/page-listing-results';
+import type { OnDuplicatedFunction, OnDeletedFunction } from '~/interfaces/ui';
+import type { UpdateDescCountData, UpdateDescCountRawData } from '~/interfaces/websocket';
+import { SocketEventName } from '~/interfaces/websocket';
+import type { IPageForPageDuplicateModal } from '~/stores/modal';
+import { usePageDuplicateModal, usePageDeleteModal } from '~/stores/modal';
 import { mutateAllPageInfo, useCurrentPagePath, useSWRMUTxCurrentPage } from '~/stores/page';
 import { mutateAllPageInfo, useCurrentPagePath, useSWRMUTxCurrentPage } from '~/stores/page';
 import {
 import {
   useSWRxPageAncestorsChildren, useSWRxRootPage, mutatePageTree, mutatePageList,
   useSWRxPageAncestorsChildren, useSWRxRootPage, mutatePageTree, mutatePageList,
@@ -31,6 +32,7 @@ import ItemsTreeContentSkeleton from './ItemsTreeContentSkeleton';
 
 
 import styles from './ItemsTree.module.scss';
 import styles from './ItemsTree.module.scss';
 
 
+
 const logger = loggerFactory('growi:cli:ItemsTree');
 const logger = loggerFactory('growi:cli:ItemsTree');
 
 
 /*
 /*
@@ -89,10 +91,12 @@ const isSecondStageRenderingCondition = (condition: RenderingCondition|SecondSta
 type ItemsTreeProps = {
 type ItemsTreeProps = {
   isEnableActions: boolean
   isEnableActions: boolean
   isReadOnlyUser: boolean
   isReadOnlyUser: boolean
+  isWipPageShown?: boolean
   targetPath: string
   targetPath: string
   targetPathOrId?: Nullable<string>
   targetPathOrId?: Nullable<string>
   targetAndAncestorsData?: TargetAndAncestors
   targetAndAncestorsData?: TargetAndAncestors
   CustomTreeItem: React.FunctionComponent<TreeItemProps>
   CustomTreeItem: React.FunctionComponent<TreeItemProps>
+  onClickTreeItem?: (page: IPageForItem) => void;
 }
 }
 
 
 /*
 /*
@@ -100,7 +104,7 @@ type ItemsTreeProps = {
  */
  */
 export const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
 export const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
   const {
   const {
-    targetPath, targetPathOrId, targetAndAncestorsData, isEnableActions, isReadOnlyUser, CustomTreeItem,
+    targetPath, targetPathOrId, targetAndAncestorsData, isEnableActions, isReadOnlyUser, isWipPageShown, CustomTreeItem, onClickTreeItem,
   } = props;
   } = props;
 
 
   const { t } = useTranslation();
   const { t } = useTranslation();
@@ -271,17 +275,19 @@ export const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
 
 
   if (initialItemNode != null) {
   if (initialItemNode != null) {
     return (
     return (
-      <ul className={`grw-pagetree ${styles['grw-pagetree']} list-group py-3`} ref={rootElemRef}>
+      <ul className={`grw-pagetree ${styles['grw-pagetree']} list-group py-4`} ref={rootElemRef}>
         <CustomTreeItem
         <CustomTreeItem
           key={initialItemNode.page.path}
           key={initialItemNode.page.path}
           targetPathOrId={targetPathOrId}
           targetPathOrId={targetPathOrId}
           itemNode={initialItemNode}
           itemNode={initialItemNode}
           isOpen
           isOpen
           isEnableActions={isEnableActions}
           isEnableActions={isEnableActions}
+          isWipPageShown={isWipPageShown}
           isReadOnlyUser={isReadOnlyUser}
           isReadOnlyUser={isReadOnlyUser}
           onRenamed={onRenamed}
           onRenamed={onRenamed}
           onClickDuplicateMenuItem={onClickDuplicateMenuItem}
           onClickDuplicateMenuItem={onClickDuplicateMenuItem}
           onClickDeleteMenuItem={onClickDeleteMenuItem}
           onClickDeleteMenuItem={onClickDeleteMenuItem}
+          onClick={onClickTreeItem}
         />
         />
       </ul>
       </ul>
     );
     );

+ 13 - 13
apps/app/src/components/Me/BasicInfoSettings.tsx

@@ -49,7 +49,7 @@ export const BasicInfoSettings = (): JSX.Element => {
   return (
   return (
     <>
     <>
 
 
-      <div className="row">
+      <div className="row mt-3 mt-md-4">
         <label htmlFor="userForm[name]" className="text-start text-md-end col-md-3 col-form-label">{t('Name')}</label>
         <label htmlFor="userForm[name]" className="text-start text-md-end col-md-3 col-form-label">{t('Name')}</label>
         <div className="col-md-6">
         <div className="col-md-6">
           <input
           <input
@@ -62,7 +62,7 @@ export const BasicInfoSettings = (): JSX.Element => {
         </div>
         </div>
       </div>
       </div>
 
 
-      <div className="row">
+      <div className="row mt-3">
         <label htmlFor="userForm[email]" className="text-start text-md-end col-md-3 col-form-label">{t('Email')}</label>
         <label htmlFor="userForm[email]" className="text-start text-md-end col-md-3 col-form-label">{t('Email')}</label>
         <div className="col-md-6">
         <div className="col-md-6">
           <input
           <input
@@ -83,10 +83,10 @@ export const BasicInfoSettings = (): JSX.Element => {
         </div>
         </div>
       </div>
       </div>
 
 
-      <div className="row">
+      <div className="row mt-3">
         <label className="text-start text-md-end col-md-3 col-form-label">{t('Disclose E-mail')}</label>
         <label className="text-start text-md-end col-md-3 col-form-label">{t('Disclose E-mail')}</label>
-        <div className="col-md-6">
-          <div className="form-check form-check-inline">
+        <div className="col-md-6 my-auto">
+          <div className="form-check form-check-inline me-4">
             <input
             <input
               type="radio"
               type="radio"
               id="radioEmailShow"
               id="radioEmailShow"
@@ -95,7 +95,7 @@ export const BasicInfoSettings = (): JSX.Element => {
               checked={personalSettingsInfo?.isEmailPublished === true}
               checked={personalSettingsInfo?.isEmailPublished === true}
               onChange={() => changePersonalSettingsHandler({ isEmailPublished: true })}
               onChange={() => changePersonalSettingsHandler({ isEmailPublished: true })}
             />
             />
-            <label className="form-label form-check-label" htmlFor="radioEmailShow">{t('Show')}</label>
+            <label className="form-label form-check-label mb-0" htmlFor="radioEmailShow">{t('Show')}</label>
           </div>
           </div>
           <div className="form-check form-check-inline">
           <div className="form-check form-check-inline">
             <input
             <input
@@ -106,21 +106,21 @@ export const BasicInfoSettings = (): JSX.Element => {
               checked={personalSettingsInfo?.isEmailPublished === false}
               checked={personalSettingsInfo?.isEmailPublished === false}
               onChange={() => changePersonalSettingsHandler({ isEmailPublished: false })}
               onChange={() => changePersonalSettingsHandler({ isEmailPublished: false })}
             />
             />
-            <label className="form-label form-check-label" htmlFor="radioEmailHide">{t('Hide')}</label>
+            <label className="form-label form-check-label mb-0" htmlFor="radioEmailHide">{t('Hide')}</label>
           </div>
           </div>
         </div>
         </div>
       </div>
       </div>
 
 
-      <div className="row">
+      <div className="row mt-3">
         <label className="text-start text-md-end col-md-3 col-form-label">{t('Language')}</label>
         <label className="text-start text-md-end col-md-3 col-form-label">{t('Language')}</label>
-        <div className="col-md-6">
+        <div className="col-md-6 my-auto">
           {
           {
             i18nConfig.locales.map((locale) => {
             i18nConfig.locales.map((locale) => {
               if (i18n == null) { return }
               if (i18n == null) { return }
               const fixedT = i18n.getFixedT(locale);
               const fixedT = i18n.getFixedT(locale);
 
 
               return (
               return (
-                <div key={locale} className="form-check form-check-inline">
+                <div key={locale} className="form-check form-check-inline me-4">
                   <input
                   <input
                     type="radio"
                     type="radio"
                     id={`radioLang${locale}`}
                     id={`radioLang${locale}`}
@@ -129,14 +129,14 @@ export const BasicInfoSettings = (): JSX.Element => {
                     checked={personalSettingsInfo?.lang === locale}
                     checked={personalSettingsInfo?.lang === locale}
                     onChange={() => changePersonalSettingsHandler({ lang: locale })}
                     onChange={() => changePersonalSettingsHandler({ lang: locale })}
                   />
                   />
-                  <label className="form-label form-check-label" htmlFor={`radioLang${locale}`}>{fixedT('meta.display_name') as string}</label>
+                  <label className="form-label form-check-label mb-0" htmlFor={`radioLang${locale}`}>{fixedT('meta.display_name') as string}</label>
                 </div>
                 </div>
               );
               );
             })
             })
           }
           }
         </div>
         </div>
       </div>
       </div>
-      <div className="row">
+      <div className="row mt-3">
         <label htmlFor="userForm[slackMemberId]" className="text-start text-md-end col-md-3 col-form-label">{t('Slack Member ID')}</label>
         <label htmlFor="userForm[slackMemberId]" className="text-start text-md-end col-md-3 col-form-label">{t('Slack Member ID')}</label>
         <div className="col-md-6">
         <div className="col-md-6">
           <input
           <input
@@ -150,7 +150,7 @@ export const BasicInfoSettings = (): JSX.Element => {
         </div>
         </div>
       </div>
       </div>
 
 
-      <div className="row my-3">
+      <div className="row mt-4">
         <div className="offset-4 col-5">
         <div className="offset-4 col-5">
           <button
           <button
             data-testid="grw-besic-info-settings-update-button"
             data-testid="grw-besic-info-settings-update-button"

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

@@ -64,7 +64,7 @@ class ExternalAccountLinkedMe extends React.Component {
             className="btn btn-outline-secondary btn-sm pull-right"
             className="btn btn-outline-secondary btn-sm pull-right"
             onClick={this.openAssociateModal}
             onClick={this.openAssociateModal}
           >
           >
-            <i className="icon-plus" aria-hidden="true" />
+            <span className="material-symbols-outlined" aria-hidden="true">add_circle</span>
             Add
             Add
           </button>
           </button>
           { t('admin:user_management.external_accounts') }
           { t('admin:user_management.external_accounts') }

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

@@ -20,22 +20,22 @@ const PersonalSettings = () => {
   const navTabMapping = useMemo(() => {
   const navTabMapping = useMemo(() => {
     return {
     return {
       user_infomation: {
       user_infomation: {
-        Icon: () => <i className="icon-fw icon-user"></i>,
+        Icon: () => <span className="material-symbols-outlined">person</span>,
         Content: UserSettings,
         Content: UserSettings,
         i18n: t('User Information'),
         i18n: t('User Information'),
       },
       },
       external_accounts: {
       external_accounts: {
-        Icon: () => <i className="icon-fw icon-share-alt"></i>,
+        Icon: () => <span className="material-symbols-outlined">ungroup</span>,
         Content: ExternalAccountLinkedMe,
         Content: ExternalAccountLinkedMe,
         i18n: t('admin:user_management.external_accounts'),
         i18n: t('admin:user_management.external_accounts'),
       },
       },
       password_settings: {
       password_settings: {
-        Icon: () => <i className="icon-fw icon-lock"></i>,
+        Icon: () => <span className="material-symbols-outlined">password</span>,
         Content: PasswordSettings,
         Content: PasswordSettings,
         i18n: t('Password Settings'),
         i18n: t('Password Settings'),
       },
       },
       api_settings: {
       api_settings: {
-        Icon: () => <i className="icon-fw icon-paper-plane"></i>,
+        Icon: () => <span className="material-symbols-outlined">api</span>,
         Content: ApiSettings,
         Content: ApiSettings,
         i18n: t('API Settings'),
         i18n: t('API Settings'),
       },
       },
@@ -45,12 +45,12 @@ const PersonalSettings = () => {
       //   i18n: t('editor_settings.editor_settings'),
       //   i18n: t('editor_settings.editor_settings'),
       // },
       // },
       in_app_notification_settings: {
       in_app_notification_settings: {
-        Icon: () => <i className="icon-fw icon-bell"></i>,
+        Icon: () => <span className="material-symbols-outlined">notifications</span>,
         Content: InAppNotificationSettings,
         Content: InAppNotificationSettings,
         i18n: t('in_app_notification_settings.in_app_notification_settings'),
         i18n: t('in_app_notification_settings.in_app_notification_settings'),
       },
       },
       other_settings: {
       other_settings: {
-        Icon: () => <i className="icon-fw icon-settings"></i>,
+        Icon: () => <span className="material-symbols-outlined">settings</span>,
         Content: OtherSettings,
         Content: OtherSettings,
         i18n: t('Other Settings'),
         i18n: t('Other Settings'),
       },
       },

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

@@ -91,9 +91,9 @@ const ProfileImageSettings = (): JSX.Element => {
 
 
   return (
   return (
     <>
     <>
-      <div className="row">
-        <div className="col-md-6 col-12 mb-3 mb-md-0">
-          <h4>
+      <div className="row justify-content-around mt-5 mt-md-4">
+        <div className="col-md-3">
+          <h5>
             <div className="form-check radio-primary">
             <div className="form-check radio-primary">
               <input
               <input
                 type="radio"
                 type="radio"
@@ -105,18 +105,18 @@ const ProfileImageSettings = (): JSX.Element => {
                 onChange={() => setGravatarEnabled(true)}
                 onChange={() => setGravatarEnabled(true)}
               />
               />
               <label className="form-label form-check-label" htmlFor="radioGravatar">
               <label className="form-label form-check-label" htmlFor="radioGravatar">
-                <img src={GRAVATAR_DEFAULT} data-vrt-blackout-profile /> Gravatar
+                <img src={GRAVATAR_DEFAULT} className="me-1" data-vrt-blackout-profile /> Gravatar
               </label>
               </label>
-              <a href="https://gravatar.com/">
-                <small><i className="icon-arrow-right-circle" aria-hidden="true"></i></small>
+              <a href="https://gravatar.com/" target="_blank" rel="noopener noreferrer">
+                <small><span className="material-symbols-outlined ms-2 text-secondary" aria-hidden="true">info</span></small>
               </a>
               </a>
             </div>
             </div>
-          </h4>
-          <img src={generateGravatarSrc(currentUser.email)} width="64" data-vrt-blackout-profile />
+          </h5>
+          <img src={generateGravatarSrc(currentUser.email)} className="rounded-pill" width="64" data-vrt-blackout-profile />
         </div>
         </div>
 
 
-        <div className="col-md-6 col-12">
-          <h4>
+        <div className="col-md-7 mt-5">
+          <h5>
             <div className="form-check radio-primary">
             <div className="form-check radio-primary">
               <input
               <input
                 type="radio"
                 type="radio"
@@ -131,21 +131,21 @@ const ProfileImageSettings = (): JSX.Element => {
                 { t('Upload Image') }
                 { t('Upload Image') }
               </label>
               </label>
             </div>
             </div>
-          </h4>
-          <div className="row mb-3">
-            <label className="col-sm-4 col-12 col-form-label text-start">
+          </h5>
+          <div className="row mt-3">
+            <label className="col-md-6 col-lg-4 col-form-label text-start">
               { t('Current Image') }
               { t('Current Image') }
             </label>
             </label>
-            <div className="col-sm-8 col-12">
-              <p><img src={uploadedPictureSrc ?? DEFAULT_IMAGE} className="picture picture-lg rounded-circle" id="settingUserPicture" /></p>
+            <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" onClick={deleteImageHandler}>{ t('Delete Image') }</button>}
             </div>
             </div>
           </div>
           </div>
-          <div className="row">
-            <label className="col-sm-4 col-12 col-form-label text-start">
+          <div className="row align-items-center mt-3 mt-md-5">
+            <label className="col-md-6 col-lg-4 col-form-label text-start mt-3 mt-md-0">
               {t('Upload new image')}
               {t('Upload new image')}
             </label>
             </label>
-            <div className="col-sm-8 col-12">
+            <div className="col-md-6 col-lg-8">
               <input type="file" onChange={selectFileHandler} name="profileImage" accept="image/*" />
               <input type="file" onChange={selectFileHandler} name="profileImage" accept="image/*" />
             </div>
             </div>
           </div>
           </div>
@@ -161,7 +161,7 @@ const ProfileImageSettings = (): JSX.Element => {
         showCropOption
         showCropOption
       />
       />
 
 
-      <div className="row my-3">
+      <div className="row mt-4">
         <div className="offset-4 col-5">
         <div className="offset-4 col-5">
           <button type="button" className="btn btn-primary" onClick={submit}>
           <button type="button" className="btn btn-primary" onClick={submit}>
             {t('Update')}
             {t('Update')}

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

@@ -11,11 +11,11 @@ const UserSettings = React.memo((): JSX.Element => {
   return (
   return (
     <div data-testid="grw-user-settings">
     <div data-testid="grw-user-settings">
       <div className="mb-5">
       <div className="mb-5">
-        <h2 className="border-bottom my-4">{t('Basic Info')}</h2>
+        <h2 className="border-bottom fs-4 mt-4 pb-1">{t('Basic Info')}</h2>
         <BasicInfoSettings />
         <BasicInfoSettings />
       </div>
       </div>
       <div className="mb-5">
       <div className="mb-5">
-        <h2 className="border-bottom my-4">{t('Set Profile Image')}</h2>
+        <h2 className="border-bottom fs-4 mt-3 mt-md-5 pb-1">{t('Set Profile Image')}</h2>
         <ProfileImageSettings />
         <ProfileImageSettings />
       </div>
       </div>
     </div>
     </div>

+ 20 - 18
apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -8,6 +8,7 @@ import type {
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
+import Link from 'next/link';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
 import { DropdownItem } from 'reactstrap';
 import { DropdownItem } from 'reactstrap';
 
 
@@ -32,10 +33,6 @@ import {
 } from '~/stores/ui';
 } from '~/stores/ui';
 
 
 import { CreateTemplateModal } from '../CreateTemplateModal';
 import { CreateTemplateModal } from '../CreateTemplateModal';
-import AttachmentIcon from '../Icons/AttachmentIcon';
-import HistoryIcon from '../Icons/HistoryIcon';
-import PresentationIcon from '../Icons/PresentationIcon';
-import ShareLinkIcon from '../Icons/ShareLinkIcon';
 import { NotAvailable } from '../NotAvailable';
 import { NotAvailable } from '../NotAvailable';
 import { Skeleton } from '../Skeleton';
 import { Skeleton } from '../Skeleton';
 
 
@@ -80,9 +77,7 @@ const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element
         data-testid="open-presentation-modal-btn"
         data-testid="open-presentation-modal-btn"
         className="grw-page-control-dropdown-item"
         className="grw-page-control-dropdown-item"
       >
       >
-        <i className="icon-fw grw-page-control-dropdown-icon">
-          <PresentationIcon />
-        </i>
+        <span className="material-symbols-outlined me-1 grw-page-control-dropdown-icon">jamboard_kiosk</span>
         {t('Presentation Mode')}
         {t('Presentation Mode')}
       </DropdownItem>
       </DropdownItem>
 
 
@@ -91,7 +86,7 @@ const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element
         onClick={() => exportAsMarkdown(pageId, revisionId, 'md')}
         onClick={() => exportAsMarkdown(pageId, revisionId, 'md')}
         className="grw-page-control-dropdown-item"
         className="grw-page-control-dropdown-item"
       >
       >
-        <i className="icon-fw icon-cloud-download grw-page-control-dropdown-icon"></i>
+        <span className="material-symbols-outlined me-1 grw-page-control-dropdown-icon">cloud_download</span>
         {t('export_bulk.export_page_markdown')}
         {t('export_bulk.export_page_markdown')}
       </DropdownItem>
       </DropdownItem>
 
 
@@ -107,9 +102,7 @@ const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element
         data-testid="open-page-accessories-modal-btn-with-history-tab"
         data-testid="open-page-accessories-modal-btn-with-history-tab"
         className="grw-page-control-dropdown-item"
         className="grw-page-control-dropdown-item"
       >
       >
-        <span className="grw-page-control-dropdown-icon">
-          <HistoryIcon />
-        </span>
+        <span className="material-symbols-outlined me-1 grw-page-control-dropdown-icon">history</span>
         {t('History')}
         {t('History')}
       </DropdownItem>
       </DropdownItem>
 
 
@@ -118,9 +111,7 @@ const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element
         data-testid="open-page-accessories-modal-btn-with-attachment-data-tab"
         data-testid="open-page-accessories-modal-btn-with-attachment-data-tab"
         className="grw-page-control-dropdown-item"
         className="grw-page-control-dropdown-item"
       >
       >
-        <span className="grw-page-control-dropdown-icon">
-          <AttachmentIcon />
-        </span>
+        <span className="material-symbols-outlined me-1 grw-page-control-dropdown-icon">attachment</span>
         {t('attachment_data')}
         {t('attachment_data')}
       </DropdownItem>
       </DropdownItem>
 
 
@@ -131,9 +122,7 @@ const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element
             data-testid="open-page-accessories-modal-btn-with-share-link-management-data-tab"
             data-testid="open-page-accessories-modal-btn-with-share-link-management-data-tab"
             className="grw-page-control-dropdown-item"
             className="grw-page-control-dropdown-item"
           >
           >
-            <span className="grw-page-control-dropdown-icon">
-              <ShareLinkIcon />
-            </span>
+            <span className="material-symbols-outlined me-1 grw-page-control-dropdown-icon">share</span>
             {t('share_links.share_link_management')}
             {t('share_links.share_link_management')}
           </DropdownItem>
           </DropdownItem>
         </NotAvailable>
         </NotAvailable>
@@ -163,7 +152,7 @@ const CreateTemplateMenuItems = (props: CreateTemplateMenuItemsProps): JSX.Eleme
         className="grw-page-control-dropdown-item"
         className="grw-page-control-dropdown-item"
         data-testid="open-page-template-modal-btn"
         data-testid="open-page-template-modal-btn"
       >
       >
-        <i className="icon-fw icon-magic-wand grw-page-control-dropdown-icon"></i>
+        <span className="material-symbols-outlined me-1 grw-page-control-dropdown-icon">contract_edit</span>
         {t('template.option_label.create/edit')}
         {t('template.option_label.create/edit')}
       </DropdownItem>
       </DropdownItem>
     </>
     </>
@@ -179,6 +168,8 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
 
 
   const { currentPage } = props;
   const { currentPage } = props;
 
 
+  const { t } = useTranslation();
+
   const router = useRouter();
   const router = useRouter();
 
 
   const { data: shareLinkId } = useShareLinkId();
   const { data: shareLinkId } = useShareLinkId();
@@ -329,6 +320,17 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
             // grantUserGroupId={grantUserGroupId}
             // grantUserGroupId={grantUserGroupId}
           />
           />
         )}
         )}
+
+        { isGuestUser && (
+          <>
+            <Link href="/login#register" className="btn" 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>
 
 
       {path != null && currentUser != null && !isReadOnlyUser && (
       {path != null && currentUser != null && !isReadOnlyUser && (

+ 26 - 23
apps/app/src/components/Navbar/PageEditorModeManager.tsx

@@ -1,12 +1,14 @@
-import React, { type ReactNode, useCallback, useState } from 'react';
+import React, { type ReactNode, useCallback } from 'react';
 
 
-import type { IGrantedGroup } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
+import { useCreatePageAndTransit } from '~/client/services/create-page';
 import { toastError } from '~/client/util/toastr';
 import { toastError } from '~/client/util/toastr';
+import { useIsNotFound } from '~/stores/page';
 import { EditorMode, useEditorMode, useIsDeviceLargerThanMd } from '~/stores/ui';
 import { EditorMode, useEditorMode, useIsDeviceLargerThanMd } from '~/stores/ui';
 
 
-import { useCreatePageAndTransit } from '../../client/services/use-create-page-and-transit';
+import { shouldCreateWipPage } from '../../utils/should-create-wip-page';
+
 
 
 import styles from './PageEditorModeManager.module.scss';
 import styles from './PageEditorModeManager.module.scss';
 
 
@@ -47,9 +49,6 @@ type Props = {
   editorMode: EditorMode | undefined,
   editorMode: EditorMode | undefined,
   isBtnDisabled: boolean,
   isBtnDisabled: boolean,
   path?: string,
   path?: string,
-  grant?: number,
-  // grantUserGroupId?: string
-  grantUserGroupIds?: IGrantedGroup[]
 }
 }
 
 
 export const PageEditorModeManager = (props: Props): JSX.Element => {
 export const PageEditorModeManager = (props: Props): JSX.Element => {
@@ -57,30 +56,34 @@ export const PageEditorModeManager = (props: Props): JSX.Element => {
     editorMode = EditorMode.View,
     editorMode = EditorMode.View,
     isBtnDisabled,
     isBtnDisabled,
     path,
     path,
-    // grant,
-    // grantUserGroupId,
   } = props;
   } = props;
 
 
-  const { t } = useTranslation('common');
-  const [isCreating, setIsCreating] = useState(false);
+  const { t } = useTranslation('commons');
 
 
+  const { data: isNotFound } = useIsNotFound();
   const { mutate: mutateEditorMode } = useEditorMode();
   const { mutate: mutateEditorMode } = useEditorMode();
   const { data: isDeviceLargerThanMd } = useIsDeviceLargerThanMd();
   const { data: isDeviceLargerThanMd } = useIsDeviceLargerThanMd();
 
 
-  const _isBtnDisabled = isCreating || isBtnDisabled;
+  const { isCreating, createAndTransit } = useCreatePageAndTransit();
+
+  const editButtonClickedHandler = useCallback(async() => {
+    if (isNotFound == null || isNotFound === false) {
+      mutateEditorMode(EditorMode.Editor);
+      return;
+    }
+
+    try {
+      await createAndTransit(
+        { path, wip: shouldCreateWipPage(path) },
+        { shouldCheckPageExists: true },
+      );
+    }
+    catch (err) {
+      toastError(t('toaster.create_failed', { target: path }));
+    }
+  }, [createAndTransit, isNotFound, mutateEditorMode, path, t]);
 
 
-  const createPageAndTransit = useCreatePageAndTransit();
-
-  const editButtonClickedHandler = useCallback(() => {
-    createPageAndTransit(
-      path,
-      {
-        onCreationStart: () => { setIsCreating(true) },
-        onError: () => { toastError(t('toaster.create_failed', { target: path })) },
-        onTerminated: () => { setIsCreating(false) },
-      },
-    );
-  }, [createPageAndTransit, path, t]);
+  const _isBtnDisabled = isCreating || isBtnDisabled;
 
 
   return (
   return (
     <>
     <>

+ 2 - 4
apps/app/src/components/NotFoundPage.tsx

@@ -4,8 +4,6 @@ import { useTranslation } from 'next-i18next';
 
 
 import CustomNavAndContents from './CustomNavigation/CustomNavAndContents';
 import CustomNavAndContents from './CustomNavigation/CustomNavAndContents';
 import { DescendantsPageList } from './DescendantsPageList';
 import { DescendantsPageList } from './DescendantsPageList';
-import PageListIcon from './Icons/PageListIcon';
-import TimeLineIcon from './Icons/TimeLineIcon';
 import { PageTimeline } from './PageTimeline';
 import { PageTimeline } from './PageTimeline';
 
 
 type NotFoundPageProps = {
 type NotFoundPageProps = {
@@ -20,12 +18,12 @@ const NotFoundPage = (props: NotFoundPageProps): JSX.Element => {
   const navTabMapping = useMemo(() => {
   const navTabMapping = useMemo(() => {
     return {
     return {
       pagelist: {
       pagelist: {
-        Icon: PageListIcon,
+        Icon: () => <span className="material-symbols-outlined">subject</span>,
         Content: () => <DescendantsPageList path={path} />,
         Content: () => <DescendantsPageList path={path} />,
         i18n: t('page_list'),
         i18n: t('page_list'),
       },
       },
       timeLine: {
       timeLine: {
-        Icon: TimeLineIcon,
+        Icon: () => <span className="material-symbols-outlined">timeline</span>,
         Content: PageTimeline,
         Content: PageTimeline,
         i18n: t('Timeline View'),
         i18n: t('Timeline View'),
       },
       },

+ 3 - 3
apps/app/src/components/Page/PageView.tsx

@@ -68,7 +68,7 @@ export const PageView = (props: Props): JSX.Element => {
   const { data: viewOptions } = useViewOptions();
   const { data: viewOptions } = useViewOptions();
 
 
   const page = pageBySWR ?? initialPage;
   const page = pageBySWR ?? initialPage;
-  const isNotFound = isNotFoundMeta || page?.revision == null;
+  const isNotFound = isNotFoundMeta || page == null;
   const isUsersHomepagePath = isUsersHomepage(pagePath);
   const isUsersHomepagePath = isUsersHomepage(pagePath);
 
 
   const shouldExpandContent = useShouldExpandContent(page);
   const shouldExpandContent = useShouldExpandContent(page);
@@ -103,7 +103,7 @@ export const PageView = (props: Props): JSX.Element => {
   }, [isForbidden, isIdenticalPathPage, isNotCreatable]);
   }, [isForbidden, isIdenticalPathPage, isNotCreatable]);
 
 
   const headerContents = (
   const headerContents = (
-    <PagePathNavSticky pageId={page?._id} pagePath={pagePath} />
+    <PagePathNavSticky pageId={page?._id} pagePath={pagePath} isWipPage={page?.wip} />
   );
   );
 
 
   const sideContents = !isNotFound && !isNotCreatable
   const sideContents = !isNotFound && !isNotCreatable
@@ -124,7 +124,7 @@ export const PageView = (props: Props): JSX.Element => {
     : null;
     : null;
 
 
   const Contents = () => {
   const Contents = () => {
-    if (isNotFound) {
+    if (isNotFound || page?.revision == null) {
       return <NotFoundPage path={pagePath} />;
       return <NotFoundPage path={pagePath} />;
     }
     }
 
 

+ 2 - 2
apps/app/src/components/Page/RevisionLoader.tsx

@@ -49,11 +49,11 @@ export const RevisionLoader = (props: RevisionLoaderProps): JSX.Element => {
     if (error != null) {
     if (error != null) {
       const isForbidden = error != null && error[0].code === 'forbidden-page';
       const isForbidden = error != null && error[0].code === 'forbidden-page';
       if (isForbidden) {
       if (isForbidden) {
-        setMarkdown(`<i class="icon-exclamation p-1"></i>${t('not_allowed_to_see_this_page')}`);
+        setMarkdown(`<span className="material-symbols-outlined p-1">cancel</span>${t('not_allowed_to_see_this_page')}`);
       }
       }
       else {
       else {
         const errorMessages = error.map((error) => {
         const errorMessages = error.map((error) => {
-          return `<i class="icon-exclamation p-1"></i><span class="text-muted"><em>${error.message}</em></span>`;
+          return `<span className="material-symbols-outlined p-1">cancel</span><span class="text-muted"><em>${error.message}</em></span>`;
         });
         });
         setMarkdown(errorMessages.join('\n'));
         setMarkdown(errorMessages.join('\n'));
       }
       }

+ 3 - 2
apps/app/src/components/Page/ShareLinkAlert.tsx

@@ -1,4 +1,5 @@
-import React, { FC } from 'react';
+import type { FC } from 'react';
+import React from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
@@ -40,7 +41,7 @@ const ShareLinkAlert: FC<Props> = (props: Props) => {
 
 
   return (
   return (
     <p className={`alert alert-${alertColor} px-4 d-edit-none`}>
     <p className={`alert alert-${alertColor} px-4 d-edit-none`}>
-      <i className="icon-fw icon-link"></i>
+      <span className="material-symbols-outlined me-1">link</span>
       {(expiredAt === null ? <span>{t('page_page.notice.no_deadline')}</span>
       {(expiredAt === null ? <span>{t('page_page.notice.no_deadline')}</span>
       // eslint-disable-next-line react/no-danger
       // eslint-disable-next-line react/no-danger
         : <span dangerouslySetInnerHTML={{ __html: t('page_page.notice.expiration', { expiredAt }) }} />
         : <span dangerouslySetInnerHTML={{ __html: t('page_page.notice.expiration', { expiredAt }) }} />

+ 3 - 6
apps/app/src/components/PageAccessoriesModal/PageAccessoriesModal.tsx

@@ -14,9 +14,6 @@ import { usePageAccessoriesModal, PageAccessoriesModalContents } from '~/stores/
 import { CustomNavTab } from '../CustomNavigation/CustomNav';
 import { CustomNavTab } from '../CustomNavigation/CustomNav';
 import CustomTabContent from '../CustomNavigation/CustomTabContent';
 import CustomTabContent from '../CustomNavigation/CustomTabContent';
 import ExpandOrContractButton from '../ExpandOrContractButton';
 import ExpandOrContractButton from '../ExpandOrContractButton';
-import AttachmentIcon from '../Icons/AttachmentIcon';
-import HistoryIcon from '../Icons/HistoryIcon';
-import ShareLinkIcon from '../Icons/ShareLinkIcon';
 
 
 import { useAutoOpenModalByQueryParam } from './hooks';
 import { useAutoOpenModalByQueryParam } from './hooks';
 
 
@@ -46,7 +43,7 @@ export const PageAccessoriesModal = (): JSX.Element => {
   const navTabMapping = useMemo(() => {
   const navTabMapping = useMemo(() => {
     return {
     return {
       [PageAccessoriesModalContents.PageHistory]: {
       [PageAccessoriesModalContents.PageHistory]: {
-        Icon: HistoryIcon,
+        Icon: () => <span className="material-symbols-outlined">history</span>,
         Content: () => {
         Content: () => {
           return <PageHistory onClose={close} />;
           return <PageHistory onClose={close} />;
         },
         },
@@ -54,14 +51,14 @@ export const PageAccessoriesModal = (): JSX.Element => {
         isLinkEnabled: () => !isGuestUser && !isSharedUser,
         isLinkEnabled: () => !isGuestUser && !isSharedUser,
       },
       },
       [PageAccessoriesModalContents.Attachment]: {
       [PageAccessoriesModalContents.Attachment]: {
-        Icon: AttachmentIcon,
+        Icon: () => <span className="material-symbols-outlined">attachment</span>,
         Content: () => {
         Content: () => {
           return <PageAttachment />;
           return <PageAttachment />;
         },
         },
         i18n: t('attachment_data'),
         i18n: t('attachment_data'),
       },
       },
       [PageAccessoriesModalContents.ShareLink]: {
       [PageAccessoriesModalContents.ShareLink]: {
-        Icon: ShareLinkIcon,
+        Icon: () => <span className="material-symbols-outlined">share</span>,
         Content: () => {
         Content: () => {
           return <ShareLink />;
           return <ShareLink />;
         },
         },

+ 1 - 1
apps/app/src/components/PageAccessoriesModal/PageAttachment.tsx

@@ -33,7 +33,7 @@ const PageAttachment = (): JSX.Element => {
   const { data: dataAttachments, remove } = useSWRxAttachments(pageId, pageNumber);
   const { data: dataAttachments, remove } = useSWRxAttachments(pageId, pageNumber);
   const { open: openDeleteAttachmentModal } = useDeleteAttachmentModal();
   const { open: openDeleteAttachmentModal } = useDeleteAttachmentModal();
   const { data: currentPage } = useSWRxCurrentPage();
   const { data: currentPage } = useSWRxCurrentPage();
-  const markdown = currentPage?.revision.body;
+  const markdown = currentPage?.revision?.body;
 
 
   // Custom hooks
   // Custom hooks
   const inUseAttachmentsMap: { [id: string]: boolean } | undefined = useMemo(() => {
   const inUseAttachmentsMap: { [id: string]: boolean } | undefined = useMemo(() => {

+ 3 - 3
apps/app/src/components/PageAlert/FixPageGrantAlert.tsx

@@ -8,8 +8,8 @@ import {
 
 
 import { apiv3Put } from '~/client/util/apiv3-client';
 import { apiv3Put } from '~/client/util/apiv3-client';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { toastError, toastSuccess } from '~/client/util/toastr';
-import { IPageGrantData } from '~/interfaces/page';
-import { PopulatedGrantedGroup, IRecordApplicableGrant, IResIsGrantNormalizedGrantData } from '~/interfaces/page-grant';
+import type { IPageGrantData } from '~/interfaces/page';
+import type { PopulatedGrantedGroup, IRecordApplicableGrant, IResIsGrantNormalizedGrantData } from '~/interfaces/page-grant';
 import { useCurrentUser } from '~/stores/context';
 import { useCurrentUser } from '~/stores/context';
 import { useSWRxApplicableGrant, useSWRxIsGrantNormalized, useSWRxCurrentPage } from '~/stores/page';
 import { useSWRxApplicableGrant, useSWRxIsGrantNormalized, useSWRxCurrentPage } from '~/stores/page';
 
 
@@ -297,7 +297,7 @@ export const FixPageGrantAlert = (): JSX.Element => {
     <>
     <>
       <div className="alert alert-warning py-3 ps-4 d-flex flex-column flex-lg-row">
       <div className="alert alert-warning py-3 ps-4 d-flex flex-column flex-lg-row">
         <div className="flex-grow-1 d-flex align-items-center">
         <div className="flex-grow-1 d-flex align-items-center">
-          <i className="icon-fw icon-exclamation ms-1" aria-hidden="true" />
+          <span className="material-symbols-outlined mx-1" aria-hidden="true">error</span>
           {t('fix_page_grant.alert.description')}
           {t('fix_page_grant.alert.description')}
         </div>
         </div>
         <div className="d-flex align-items-end align-items-lg-center">
         <div className="d-flex align-items-end align-items-lg-center">

+ 1 - 1
apps/app/src/components/PageAlert/OldRevisionAlert.tsx

@@ -20,7 +20,7 @@ export const OldRevisionAlert = (): JSX.Element => {
     <div className="alert alert-warning">
     <div className="alert alert-warning">
       <strong>{t('Warning')}: </strong> {t('page_page.notice.version')}
       <strong>{t('Warning')}: </strong> {t('page_page.notice.version')}
       <Link href={returnPathForURL(page.path, page._id)}>
       <Link href={returnPathForURL(page.path, page._id)}>
-        <i className="icon-fw icon-arrow-right-circle"></i>{t('Show latest')}
+        <span className="material-symbols-outlined me-1">arrow_circle_right</span>{t('Show latest')}
       </Link>
       </Link>
     </div>
     </div>
   );
   );

+ 2 - 0
apps/app/src/components/PageAlert/PageAlerts.tsx

@@ -8,6 +8,7 @@ import { OldRevisionAlert } from './OldRevisionAlert';
 import { PageGrantAlert } from './PageGrantAlert';
 import { PageGrantAlert } from './PageGrantAlert';
 import { PageRedirectedAlert } from './PageRedirectedAlert';
 import { PageRedirectedAlert } from './PageRedirectedAlert';
 import { PageStaleAlert } from './PageStaleAlert';
 import { PageStaleAlert } from './PageStaleAlert';
+import { WipPageAlert } from './WipPageAlert';
 
 
 const FixPageGrantAlert = dynamic(() => import('./FixPageGrantAlert').then(mod => mod.FixPageGrantAlert), { ssr: false });
 const FixPageGrantAlert = dynamic(() => import('./FixPageGrantAlert').then(mod => mod.FixPageGrantAlert), { ssr: false });
 // dynamic import because TrashPageAlert uses localStorageMiddleware
 // dynamic import because TrashPageAlert uses localStorageMiddleware
@@ -22,6 +23,7 @@ export const PageAlerts = (): JSX.Element => {
       <div className="col-sm-12">
       <div className="col-sm-12">
         {/* alerts */}
         {/* alerts */}
         { !isNotFound && <FixPageGrantAlert /> }
         { !isNotFound && <FixPageGrantAlert /> }
+        <WipPageAlert />
         <PageGrantAlert />
         <PageGrantAlert />
         <TrashPageAlert />
         <TrashPageAlert />
         <PageStaleAlert />
         <PageStaleAlert />

+ 3 - 3
apps/app/src/components/PageAlert/PageGrantAlert.tsx

@@ -23,21 +23,21 @@ export const PageGrantAlert = (): JSX.Element => {
       if (pageData.grant === 2) {
       if (pageData.grant === 2) {
         return (
         return (
           <>
           <>
-            <i className="icon-fw icon-link"></i><strong>{t('Anyone with the link')}</strong>
+            <span className="material-symbols-outlined me-1">link</span><strong>{t('Anyone with the link')}</strong>
           </>
           </>
         );
         );
       }
       }
       if (pageData.grant === 4) {
       if (pageData.grant === 4) {
         return (
         return (
           <>
           <>
-            <i className="icon-fw icon-lock"></i><strong>{t('Only me')}</strong>
+            <span className="material-symbols-outlined me-1">lock</span><strong>{t('Only me')}</strong>
           </>
           </>
         );
         );
       }
       }
       if (pageData.grant === 5) {
       if (pageData.grant === 5) {
         return (
         return (
           <>
           <>
-            <i className="icon-fw icon-organization"></i>
+            <span className="material-symbols-outlined me-1">account_tree</span>
             <strong>{
             <strong>{
               populatedGrantedGroups().map(g => g.item.name).join(', ')
               populatedGrantedGroups().map(g => g.item.name).join(', ')
             }
             }

+ 1 - 1
apps/app/src/components/PageAlert/PageStaleAlert.tsx

@@ -37,7 +37,7 @@ export const PageStaleAlert = ():JSX.Element => {
 
 
   return (
   return (
     <div className={`alert ${alertClass}`}>
     <div className={`alert ${alertClass}`}>
-      <i className="icon-fw icon-hourglass"></i>
+      <span className="material-symbols-outlined me-1">hourglass</span>
       <strong>{ t('page_page.notice.stale', { count: contentAge }) }</strong>
       <strong>{ t('page_page.notice.stale', { count: contentAge }) }</strong>
     </div>
     </div>
   );
   );

+ 1 - 1
apps/app/src/components/PageAlert/TrashPageAlert.tsx

@@ -90,7 +90,7 @@ export const TrashPageAlert = (): JSX.Element => {
           onClick={openPutbackPageModalHandler}
           onClick={openPutbackPageModalHandler}
           data-testid="put-back-button"
           data-testid="put-back-button"
         >
         >
-          <i className="icon-action-undo" aria-hidden="true"></i> {t('Put Back')}
+          <span className="material-symbols-outlined" aria-hidden="true">undo</span> {t('Put Back')}
         </button>
         </button>
         <button
         <button
           type="button"
           type="button"

+ 53 - 0
apps/app/src/components/PageAlert/WipPageAlert.tsx

@@ -0,0 +1,53 @@
+import React, { useCallback } from 'react';
+
+import { useTranslation } from 'react-i18next';
+
+import { toastSuccess, toastError } from '~/client/util/toastr';
+import { useSWRMUTxCurrentPage, useSWRxCurrentPage } from '~/stores/page';
+import { mutatePageTree } from '~/stores/page-listing';
+
+import { publish } from '../../client/services/page-operation';
+
+
+export const WipPageAlert = (): JSX.Element => {
+  const { t } = useTranslation();
+  const { data: currentPage } = useSWRxCurrentPage();
+  const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
+
+  const clickPagePublishButton = useCallback(async() => {
+    const pageId = currentPage?._id;
+
+    if (pageId == null) {
+      return;
+    }
+
+    try {
+      await publish(pageId);
+      await mutateCurrentPage();
+      await mutatePageTree();
+      toastSuccess(t('wip_page.success_publish_page'));
+    }
+    catch {
+      toastError(t('wip_page.fail_publish_page'));
+    }
+  }, [currentPage?._id, mutateCurrentPage, t]);
+
+
+  if (!currentPage?.wip) {
+    return <></>;
+  }
+
+  return (
+    <p className="d-flex align-items-center alert alert-secondary py-3 px-4">
+      <span className="material-symbols-outlined me-1 fs-5">info</span>
+      <span>{t('wip_page.alert')}</span>
+      <button
+        type="button"
+        className="btn btn-outline-secondary ms-auto"
+        onClick={clickPagePublishButton}
+      >
+        {t('wip_page.publish_page') }
+      </button>
+    </p>
+  );
+};

+ 2 - 2
apps/app/src/components/PageAttachment/DeleteAttachmentModal.tsx

@@ -20,7 +20,7 @@ import styles from './DeleteAttachmentModal.module.scss';
 const logger = loggerFactory('growi:attachmentDelete');
 const logger = loggerFactory('growi:attachmentDelete');
 
 
 const iconByFormat = (format: string): string => {
 const iconByFormat = (format: string): string => {
-  return format.match(/image\/.+/i) ? 'icon-picture' : 'icon-doc';
+  return format.match(/image\/.+/i) ? 'image' : 'description';
 };
 };
 
 
 export const DeleteAttachmentModal: React.FC = () => {
 export const DeleteAttachmentModal: React.FC = () => {
@@ -73,7 +73,7 @@ export const DeleteAttachmentModal: React.FC = () => {
     return (
     return (
       <div className="attachment-delete-image">
       <div className="attachment-delete-image">
         <p>
         <p>
-          <i className={iconByFormat(attachment.fileFormat)}></i> {attachment.originalName}
+          <span className="material-symbols-outlined">{iconByFormat(attachment.fileFormat)}</span> {attachment.originalName}
         </p>
         </p>
         <p>
         <p>
           uploaded by <UserPicture user={attachment.creator} size="sm"></UserPicture> <Username user={attachment.creator as IUser}></Username>
           uploaded by <UserPicture user={attachment.creator} size="sm"></UserPicture> <Username user={attachment.creator as IUser}></Username>

+ 2 - 3
apps/app/src/components/PageComment/Comment.tsx

@@ -12,9 +12,8 @@ import urljoin from 'url-join';
 import type { RendererOptions } from '~/interfaces/renderer-options';
 import type { RendererOptions } from '~/interfaces/renderer-options';
 
 
 
 
-import { ICommentHasId } from '../../interfaces/comment';
+import type { ICommentHasId } from '../../interfaces/comment';
 import FormattedDistanceDate from '../FormattedDistanceDate';
 import FormattedDistanceDate from '../FormattedDistanceDate';
-import HistoryIcon from '../Icons/HistoryIcon';
 import RevisionRenderer from '../Page/RevisionRenderer';
 import RevisionRenderer from '../Page/RevisionRenderer';
 import { Username } from '../User/Username';
 import { Username } from '../User/Username';
 
 
@@ -177,7 +176,7 @@ export const Comment = (props: CommentProps): JSX.Element => {
                   className="page-comment-revision"
                   className="page-comment-revision"
                   prefetch={false}
                   prefetch={false}
                 >
                 >
-                  <HistoryIcon />
+                  <span className="material-symbols-outlined">history</span>
                 </Link>
                 </Link>
                 <UncontrolledTooltip placement="bottom" fade={false} target={`page-comment-revision-${commentId}`}>
                 <UncontrolledTooltip placement="bottom" fade={false} target={`page-comment-revision-${commentId}`}>
                   {t('page_comment.display_the_page_when_posting_this_comment')}
                   {t('page_comment.display_the_page_when_posting_this_comment')}

+ 48 - 47
apps/app/src/components/PageComment/CommentEditor.tsx

@@ -12,17 +12,19 @@ import {
   Button, TabContent, TabPane,
   Button, TabContent, TabPane,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
-import { apiPostForm } from '~/client/util/apiv1-client';
+import { apiv3Get, apiv3PostForm } from '~/client/util/apiv3-client';
 import { toastError } from '~/client/util/toastr';
 import { toastError } from '~/client/util/toastr';
 import type { IEditorMethods } from '~/interfaces/editor-methods';
 import type { IEditorMethods } from '~/interfaces/editor-methods';
 import { useSWRxPageComment, useSWRxEditingCommentsNum } from '~/stores/comment';
 import { useSWRxPageComment, useSWRxEditingCommentsNum } from '~/stores/comment';
 import {
 import {
-  useCurrentUser, useIsSlackConfigured,
-  useIsUploadAllFileAllowed, useIsUploadEnabled,
+  useCurrentUser, useIsSlackConfigured, useAcceptedUploadFileType,
 } from '~/stores/context';
 } from '~/stores/context';
-import { useSWRxSlackChannels, useIsSlackEnabled, useIsEnabledUnsavedWarning } from '~/stores/editor';
+import {
+  useSWRxSlackChannels, useIsSlackEnabled, useIsEnabledUnsavedWarning,
+} from '~/stores/editor';
 import { useCurrentPagePath } from '~/stores/page';
 import { useCurrentPagePath } from '~/stores/page';
 import { useNextThemes } from '~/stores/use-next-themes';
 import { useNextThemes } from '~/stores/use-next-themes';
+import loggerFactory from '~/utils/logger';
 
 
 import { CustomNavTab } from '../CustomNavigation/CustomNav';
 import { CustomNavTab } from '../CustomNavigation/CustomNav';
 import { NotAvailableForGuest } from '../NotAvailableForGuest';
 import { NotAvailableForGuest } from '../NotAvailableForGuest';
@@ -30,19 +32,23 @@ import { NotAvailableForReadOnlyUser } from '../NotAvailableForReadOnlyUser';
 
 
 import { CommentPreview } from './CommentPreview';
 import { CommentPreview } from './CommentPreview';
 
 
+import '@growi/editor/dist/style.css';
 import styles from './CommentEditor.module.scss';
 import styles from './CommentEditor.module.scss';
 
 
 
 
+const logger = loggerFactory('growi:components:CommentEditor');
+
+
 const SlackNotification = dynamic(() => import('../SlackNotification').then(mod => mod.SlackNotification), { ssr: false });
 const SlackNotification = dynamic(() => import('../SlackNotification').then(mod => mod.SlackNotification), { ssr: false });
 
 
 
 
 const navTabMapping = {
 const navTabMapping = {
   comment_editor: {
   comment_editor: {
-    Icon: () => <i className="icon-settings" />,
+    Icon: () => <span className="material-symbols-outlined">edit_square</span>,
     i18n: 'Write',
     i18n: 'Write',
   },
   },
   comment_preview: {
   comment_preview: {
-    Icon: () => <i className="icon-settings" />,
+    Icon: () => <span className="material-symbols-outlined">play_arrow</span>,
     i18n: 'Preview',
     i18n: 'Preview',
   },
   },
 };
 };
@@ -70,10 +76,9 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPagePath } = useCurrentPagePath();
   const { update: updateComment, post: postComment } = useSWRxPageComment(pageId);
   const { update: updateComment, post: postComment } = useSWRxPageComment(pageId);
   const { data: isSlackEnabled, mutate: mutateIsSlackEnabled } = useIsSlackEnabled();
   const { data: isSlackEnabled, mutate: mutateIsSlackEnabled } = useIsSlackEnabled();
+  const { data: acceptedUploadFileType } = useAcceptedUploadFileType();
   const { data: slackChannelsData } = useSWRxSlackChannels(currentPagePath);
   const { data: slackChannelsData } = useSWRxSlackChannels(currentPagePath);
   const { data: isSlackConfigured } = useIsSlackConfigured();
   const { data: isSlackConfigured } = useIsSlackConfigured();
-  const { data: isUploadAllFileAllowed } = useIsUploadAllFileAllowed();
-  const { data: isUploadEnabled } = useIsUploadEnabled();
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
   const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
   const {
   const {
     increment: incrementEditingCommentsNum,
     increment: incrementEditingCommentsNum,
@@ -201,48 +206,43 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
     updateComment, comment, revisionId, replyTo, isSlackEnabled, slackChannels, postComment,
     updateComment, comment, revisionId, replyTo, isSlackEnabled, slackChannels, postComment,
   ]);
   ]);
 
 
-  const ctrlEnterHandler = useCallback((event) => {
-    if (event != null) {
-      event.preventDefault();
-    }
+  // the upload event handler
+  const uploadHandler = useCallback((files: File[]) => {
+    files.forEach(async(file) => {
+      try {
+        const { data: resLimit } = await apiv3Get('/attachment/limit', { fileSize: file.size });
 
 
-    postCommentHandler();
-  }, [postCommentHandler]);
+        if (!resLimit.isUploadable) {
+          throw new Error(resLimit.errorMessage);
+        }
 
 
-  const apiErrorHandler = useCallback((error: Error) => {
-    toastError(error.message);
-  }, []);
+        const formData = new FormData();
+        formData.append('file', file);
+        if (pageId != null) {
+          formData.append('page_id', pageId);
+        }
 
 
-  const uploadHandler = useCallback(async(file) => {
-    if (editorRef.current == null) { return }
+        const { data: resAdd } = await apiv3PostForm('/attachment', formData);
 
 
-    const pagePath = currentPagePath;
-    const endpoint = '/attachments.add';
-    const formData = new FormData();
-    formData.append('file', file);
-    formData.append('path', pagePath ?? '');
-    formData.append('page_id', pageId ?? '');
+        const attachment = resAdd.attachment;
+        const fileName = attachment.originalName;
 
 
-    try {
-      // TODO: typescriptize res
-      const res = await apiPostForm(endpoint, formData) as any;
-      const attachment = res.attachment;
-      const fileName = attachment.originalName;
-      let insertText = `[${fileName}](${attachment.filePathProxied})`;
-      // when image
-      if (attachment.fileFormat.startsWith('image/')) {
-        // modify to "![fileName](url)" syntax
-        insertText = `!${insertText}`;
+        let insertText = `[${fileName}](${attachment.filePathProxied})\n`;
+        // when image
+        if (attachment.fileFormat.startsWith('image/')) {
+          // modify to "![fileName](url)" syntax
+          insertText = `!${insertText}`;
+        }
+
+        codeMirrorEditor?.insertText(insertText);
       }
       }
-      editorRef.current.insertText(insertText);
-    }
-    catch (err) {
-      apiErrorHandler(err);
-    }
-    finally {
-      editorRef.current.terminateUploadingState();
-    }
-  }, [apiErrorHandler, currentPagePath, pageId]);
+      catch (e) {
+        logger.error('failed to upload', e);
+        toastError(e);
+      }
+    });
+
+  }, [codeMirrorEditor, pageId]);
 
 
   const getCommentHtml = useCallback(() => {
   const getCommentHtml = useCallback(() => {
     if (currentPagePath == null) {
     if (currentPagePath == null) {
@@ -263,7 +263,7 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
               onClick={() => setIsReadyToUse(true)}
               onClick={() => setIsReadyToUse(true)}
               data-testid="open-comment-editor-button"
               data-testid="open-comment-editor-button"
             >
             >
-              <i className="icon-bubble"></i> Add Comment
+              <span className="material-symbols-outlined">comment</span> Add Comment
             </button>
             </button>
           </NotAvailableForReadOnlyUser>
           </NotAvailableForReadOnlyUser>
         </NotAvailableForGuest>
         </NotAvailableForGuest>
@@ -325,8 +325,6 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
       </Button>
       </Button>
     );
     );
 
 
-    const isUploadable = isUploadEnabled || isUploadAllFileAllowed;
-
     return (
     return (
       <>
       <>
         <div className="comment-write">
         <div className="comment-write">
@@ -334,7 +332,10 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
           <TabContent activeTab={activeTab}>
           <TabContent activeTab={activeTab}>
             <TabPane tabId="comment_editor">
             <TabPane tabId="comment_editor">
               <CodeMirrorEditorComment
               <CodeMirrorEditorComment
+                acceptedUploadFileType={acceptedUploadFileType}
                 onChange={onChangeHandler}
                 onChange={onChangeHandler}
+                onSave={postCommentHandler}
+                onUpload={uploadHandler}
               />
               />
               {/* <Editor
               {/* <Editor
                 ref={editorRef}
                 ref={editorRef}

+ 2 - 2
apps/app/src/components/PageComment/ReplyComments.tsx

@@ -68,8 +68,8 @@ export const ReplyComments = (props: ReplycommentsProps): JSX.Element => {
   }
   }
 
 
   const areThereHiddenReplies = (replyList.length > 2);
   const areThereHiddenReplies = (replyList.length > 2);
-  const toggleButtonIconName = isOlderRepliesShown ? 'icon-arrow-up' : 'icon-options-vertical';
-  const toggleButtonIcon = <i className={`icon-fw ${toggleButtonIconName}`}></i>;
+  const toggleButtonIconName = isOlderRepliesShown ? 'expand_less' : 'more_vert';
+  const toggleButtonIcon = <span className="material-icons-outlined me-1">{toggleButtonIconName}</span>;
   const toggleButtonLabel = isOlderRepliesShown ? '' : 'more';
   const toggleButtonLabel = isOlderRepliesShown ? '' : 'more';
   const shownReplies = replyList.slice(replyList.length - 2, replyList.length);
   const shownReplies = replyList.slice(replyList.length - 2, replyList.length);
   const hiddenReplies = replyList.slice(0, replyList.length - 2);
   const hiddenReplies = replyList.slice(0, replyList.length - 2);

+ 4 - 0
apps/app/src/components/PageContentFooter.tsx

@@ -21,6 +21,10 @@ export const PageContentFooter = (props: PageContentFooterProps): JSX.Element =>
     creator, lastUpdateUser, createdAt, updatedAt,
     creator, lastUpdateUser, createdAt, updatedAt,
   } = page;
   } = page;
 
 
+  if (page.isEmpty) {
+    return <></>;
+  }
+
   return (
   return (
     <div className={`${styles['page-content-footer']} page-content-footer py-4 d-edit-none d-print-none}`}>
     <div className={`${styles['page-content-footer']} page-content-footer py-4 d-edit-none d-print-none}`}>
       <div className="container-lg grw-container-convertible">
       <div className="container-lg grw-container-convertible">

+ 9 - 8
apps/app/src/components/PageControls/PageControls.tsx

@@ -20,8 +20,9 @@ import loggerFactory from '~/utils/logger';
 
 
 import { useSWRxPageInfo, useSWRxTagsInfo } from '../../stores/page';
 import { useSWRxPageInfo, useSWRxTagsInfo } from '../../stores/page';
 import { useSWRxUsersList } from '../../stores/user';
 import { useSWRxUsersList } from '../../stores/user';
+import type { AdditionalMenuItemsRendererProps, ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 import {
 import {
-  AdditionalMenuItemsRendererProps, ForceHideMenuItems, MenuItemType,
+  MenuItemType,
   PageItemControl,
   PageItemControl,
 } from '../Common/Dropdown/PageItemControl';
 } from '../Common/Dropdown/PageItemControl';
 
 
@@ -45,13 +46,13 @@ const Tags = (props: TagsProps): JSX.Element => {
   const { onClickEditTagsButton } = props;
   const { onClickEditTagsButton } = props;
 
 
   return (
   return (
-    <div className="grw-taglabels-container d-flex align-items-center">
+    <div className="grw-tag-labels-container d-flex align-items-center">
       <button
       <button
         type="button"
         type="button"
         className="btn btn-link btn-edit-tags text-muted border border-secondary p-1 d-flex align-items-center"
         className="btn btn-link btn-edit-tags text-muted border border-secondary p-1 d-flex align-items-center"
         onClick={onClickEditTagsButton}
         onClick={onClickEditTagsButton}
       >
       >
-        <i className="icon-tag me-2" />
+        <span className="material-symbols-outlined me-2">local_offer</span>
         Tags
         Tags
       </button>
       </button>
     </div>
     </div>
@@ -106,7 +107,7 @@ type CommonProps = {
 type PageControlsSubstanceProps = CommonProps & {
 type PageControlsSubstanceProps = CommonProps & {
   pageId: string,
   pageId: string,
   shareLinkId?: string | null,
   shareLinkId?: string | null,
-  revisionId: string | null,
+  revisionId?: string | null,
   path?: string | null,
   path?: string | null,
   pageInfo: IPageInfoForOperation,
   pageInfo: IPageInfoForOperation,
   expandContentWidth?: boolean,
   expandContentWidth?: boolean,
@@ -177,7 +178,7 @@ const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element =
     const page: IPageToRenameWithMeta = {
     const page: IPageToRenameWithMeta = {
       data: {
       data: {
         _id: pageId,
         _id: pageId,
-        revision: revisionId,
+        revision: revisionId ?? null,
         path,
         path,
       },
       },
       meta: pageInfo,
       meta: pageInfo,
@@ -194,7 +195,7 @@ const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element =
     const pageToDelete: IPageToDeleteWithMeta = {
     const pageToDelete: IPageToDeleteWithMeta = {
       data: {
       data: {
         _id: pageId,
         _id: pageId,
-        revision: revisionId,
+        revision: revisionId ?? null,
         path,
         path,
       },
       },
       meta: pageInfo,
       meta: pageInfo,
@@ -310,7 +311,7 @@ const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element =
 type PageControlsProps = CommonProps & {
 type PageControlsProps = CommonProps & {
   pageId: string,
   pageId: string,
   shareLinkId?: string | null,
   shareLinkId?: string | null,
-  revisionId?: string,
+  revisionId?: string | null,
   path?: string | null,
   path?: string | null,
   expandContentWidth?: boolean,
   expandContentWidth?: boolean,
 };
 };
@@ -345,7 +346,7 @@ export const PageControls = memo((props: PageControlsProps): JSX.Element => {
       {...props}
       {...props}
       pageInfo={pageInfo}
       pageInfo={pageInfo}
       pageId={pageId}
       pageId={pageId}
-      revisionId={revisionId ?? null}
+      revisionId={revisionId}
       path={path}
       path={path}
       onClickEditTagsButton={onClickEditTagsButton}
       onClickEditTagsButton={onClickEditTagsButton}
       onClickDuplicateMenuItem={onClickDuplicateMenuItem}
       onClickDuplicateMenuItem={onClickDuplicateMenuItem}

+ 8 - 8
apps/app/src/components/PageDuplicateModal.tsx

@@ -160,10 +160,10 @@ const PageDuplicateModal = (): JSX.Element => {
 
 
     return (
     return (
       <>
       <>
-        <div><label className="form-label">{t('modal_duplicate.label.Current page name')}</label><br />
+        <div className="mt-3"><label className="form-label">{t('modal_duplicate.label.Current page name')}</label><br />
           <code>{path}</code>
           <code>{path}</code>
         </div>
         </div>
-        <div>
+        <div className="mt-3">
           <label className="form-label" htmlFor="duplicatePageName">{ t('modal_duplicate.label.New page name') }</label><br />
           <label className="form-label" htmlFor="duplicatePageName">{ t('modal_duplicate.label.New page name') }</label><br />
           <div className="input-group">
           <div className="input-group">
             <div>
             <div>
@@ -196,7 +196,7 @@ const PageDuplicateModal = (): JSX.Element => {
           <p className="text-danger">Error: Target path is duplicated.</p>
           <p className="text-danger">Error: Target path is duplicated.</p>
         ) }
         ) }
 
 
-        <div className="form-check form-check-warning">
+        <div className="form-check form-check-warning mt-3">
           <input
           <input
             className="form-check-input"
             className="form-check-input"
             name="recursively"
             name="recursively"
@@ -210,7 +210,7 @@ const PageDuplicateModal = (): JSX.Element => {
             <p className="form-text text-muted my-0">{ t('modal_duplicate.help.recursive') }</p>
             <p className="form-text text-muted my-0">{ t('modal_duplicate.help.recursive') }</p>
           </label>
           </label>
 
 
-          <div>
+          <div className="mt-3">
             {isDuplicateRecursively && existingPaths.length !== 0 && (
             {isDuplicateRecursively && existingPaths.length !== 0 && (
               <div className="form-check form-check-warning">
               <div className="form-check form-check-warning">
                 <input
                 <input
@@ -230,7 +230,7 @@ const PageDuplicateModal = (): JSX.Element => {
           </div>
           </div>
         </div>
         </div>
 
 
-        <div className="form-check form-check-warning mb-3">
+        <div className="form-check form-check-warning mt-2">
           <input
           <input
             className="form-check-input"
             className="form-check-input"
             id="cbOnlyDuplicateUserRelatedResources"
             id="cbOnlyDuplicateUserRelatedResources"
@@ -239,11 +239,11 @@ const PageDuplicateModal = (): JSX.Element => {
             onChange={() => setOnlyDuplicateUserRelatedResources(!onlyDuplicateUserRelatedResources)}
             onChange={() => setOnlyDuplicateUserRelatedResources(!onlyDuplicateUserRelatedResources)}
           />
           />
           <label className="form-label form-check-label" htmlFor="cbOnlyDuplicateUserRelatedResources">
           <label className="form-label form-check-label" htmlFor="cbOnlyDuplicateUserRelatedResources">
-            { t('modal_duplicate.label.Only duplicate user related resources') }
-            <p className="form-text text-muted my-0">{ t('modal_duplicate.help.only_user_related_resources') }</p>
+            { t('modal_duplicate.label.Only duplicate user related pages') }
+            <p className="form-text text-muted my-0">{ t('modal_duplicate.help.only_inherit_user_related_groups') }</p>
           </label>
           </label>
         </div>
         </div>
-        <div>
+        <div className="mt-3">
           {isDuplicateRecursively && existingPaths.length !== 0 && (
           {isDuplicateRecursively && existingPaths.length !== 0 && (
             <DuplicatePathsTable existingPaths={existingPaths} fromPath={path} toPath={pageNameInput} />
             <DuplicatePathsTable existingPaths={existingPaths} fromPath={path} toPath={pageNameInput} />
           ) }
           ) }

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