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

Merge branch 'dev/7.0.x' into imprv/133158-139808-update-react-bootstrap-typeahed

ryoji-s 2 лет назад
Родитель
Сommit
ac1e4567b9
100 измененных файлов с 559 добавлено и 660 удалено
  1. 4 3
      apps/app/public/static/locales/en_US/translation.json
  2. 4 3
      apps/app/public/static/locales/ja_JP/translation.json
  3. 4 3
      apps/app/public/static/locales/zh_CN/translation.json
  4. 1 1
      apps/app/src/client/services/page-operation.ts
  5. 1 1
      apps/app/src/client/services/side-effects/drawio-modal-launcher-for-view.ts
  6. 3 1
      apps/app/src/client/services/side-effects/handsontable-modal-launcher-for-view.ts
  7. 2 2
      apps/app/src/client/util/bookmark-utils.ts
  8. 4 5
      apps/app/src/components/Admin/Security/GitHubSecuritySettingContents.jsx
  9. 4 4
      apps/app/src/components/Admin/Security/GoogleSecuritySettingContents.jsx
  10. 6 6
      apps/app/src/components/Admin/Security/OidcSecuritySettingContents.jsx
  11. 3 3
      apps/app/src/components/Admin/Security/SamlSecuritySettingContents.jsx
  12. 4 4
      apps/app/src/components/Admin/Security/SecuritySetting.jsx
  13. 1 1
      apps/app/src/components/Admin/SlackIntegration/SlackIntegration.jsx
  14. 4 5
      apps/app/src/components/Admin/UserGroup/UserGroupTable.tsx
  15. 3 2
      apps/app/src/components/Admin/UserGroupDetail/UpdateParentConfirmModal.tsx
  16. 1 1
      apps/app/src/components/Admin/UserGroupDetail/UserGroupUserTable.tsx
  17. 1 1
      apps/app/src/components/Admin/Users/ExternalAccountTable.tsx
  18. 1 1
      apps/app/src/components/Admin/Users/GrantAdminButton.tsx
  19. 1 1
      apps/app/src/components/Admin/Users/GrantReadOnlyButton.tsx
  20. 2 2
      apps/app/src/components/Admin/Users/RevokeAdminButton.tsx
  21. 2 2
      apps/app/src/components/Admin/Users/RevokeAdminMenuItem.tsx
  22. 1 1
      apps/app/src/components/Admin/Users/RevokeReadOnlyMenuItem.tsx
  23. 1 1
      apps/app/src/components/Admin/Users/SendInvitationEmailButton.jsx
  24. 1 1
      apps/app/src/components/Admin/Users/StatusActivateButton.jsx
  25. 2 2
      apps/app/src/components/Admin/Users/StatusSuspendMenuItem.tsx
  26. 1 1
      apps/app/src/components/Admin/Users/UserMenu.tsx
  27. 2 2
      apps/app/src/components/Bookmarks/BookmarkFolderItemControl.tsx
  28. 6 6
      apps/app/src/components/Common/Dropdown/PageItemControl.tsx
  29. 1 1
      apps/app/src/components/ContentLinkButtons.tsx
  30. 0 28
      apps/app/src/components/Icons/AttachmentIcon.jsx
  31. 0 22
      apps/app/src/components/Icons/HistoryIcon.jsx
  32. 0 22
      apps/app/src/components/Icons/PresentationIcon.jsx
  33. 0 35
      apps/app/src/components/Icons/ShareLinkIcon.jsx
  34. 6 18
      apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  35. 2 2
      apps/app/src/components/Page/PageView.tsx
  36. 2 2
      apps/app/src/components/Page/RevisionLoader.tsx
  37. 3 2
      apps/app/src/components/Page/ShareLinkAlert.tsx
  38. 3 6
      apps/app/src/components/PageAccessoriesModal/PageAccessoriesModal.tsx
  39. 1 1
      apps/app/src/components/PageAccessoriesModal/PageAttachment.tsx
  40. 3 3
      apps/app/src/components/PageAlert/FixPageGrantAlert.tsx
  41. 1 1
      apps/app/src/components/PageAlert/OldRevisionAlert.tsx
  42. 3 3
      apps/app/src/components/PageAlert/PageGrantAlert.tsx
  43. 1 1
      apps/app/src/components/PageAlert/PageStaleAlert.tsx
  44. 1 1
      apps/app/src/components/PageAlert/TrashPageAlert.tsx
  45. 2 3
      apps/app/src/components/PageComment/Comment.tsx
  46. 4 0
      apps/app/src/components/PageContentFooter.tsx
  47. 5 5
      apps/app/src/components/PageControls/PageControls.tsx
  48. 2 2
      apps/app/src/components/PageDuplicateModal.tsx
  49. 6 5
      apps/app/src/components/PageEditor/Editor.tsx
  50. 2 31
      apps/app/src/components/PageEditor/EditorNavbarBottom.tsx
  51. 211 170
      apps/app/src/components/PageEditor/OptionsSelector.tsx
  52. 2 14
      apps/app/src/components/PageEditor/PageEditor.tsx
  53. 1 1
      apps/app/src/components/PageEditor/page-path-rename-utils.ts
  54. 1 1
      apps/app/src/components/PageHistory/Revision.tsx
  55. 2 2
      apps/app/src/components/PageHistory/RevisionDiff.tsx
  56. 9 7
      apps/app/src/components/PageList/PageListItemL.tsx
  57. 9 9
      apps/app/src/components/PageManagement/ApiErrorMessage.jsx
  58. 1 1
      apps/app/src/components/PagePresentationModal.tsx
  59. 8 5
      apps/app/src/components/PageSideContents/PageSideContents.tsx
  60. 1 1
      apps/app/src/components/PageStatusAlert.tsx
  61. 1 1
      apps/app/src/components/PageTimeline.tsx
  62. 2 2
      apps/app/src/components/ReactMarkdownComponents/DrawioViewerWithEditButton.tsx
  63. 4 4
      apps/app/src/components/ReactMarkdownComponents/Header.tsx
  64. 1 1
      apps/app/src/components/ReactMarkdownComponents/RichAttachment.tsx
  65. 3 3
      apps/app/src/components/ReactMarkdownComponents/TableWithEditButton.tsx
  66. 2 2
      apps/app/src/components/SavePageControls/GrantSelector/GrantSelector.tsx
  67. 8 17
      apps/app/src/components/SearchPage.tsx
  68. 5 5
      apps/app/src/components/SearchPage/OperateAllControl.tsx
  69. 9 0
      apps/app/src/components/SearchPage/SearchControl.module.scss
  70. 24 22
      apps/app/src/components/SearchPage/SearchControl.tsx
  71. 4 3
      apps/app/src/components/SearchPage/SearchOptionModal.tsx
  72. 18 19
      apps/app/src/components/SearchPage/SearchResultContent.tsx
  73. 3 0
      apps/app/src/components/SearchPage/SortControl.module.scss
  74. 32 35
      apps/app/src/components/SearchPage/SortControl.tsx
  75. 1 1
      apps/app/src/components/ShareLinkPageView.tsx
  76. 2 0
      apps/app/src/components/Sidebar/Bookmarks.tsx
  77. 8 5
      apps/app/src/components/Sidebar/Bookmarks/BookmarkContents.tsx
  78. 2 0
      apps/app/src/components/Sidebar/Custom/CustomSidebar.tsx
  79. 1 1
      apps/app/src/components/Sidebar/Custom/CustomSidebarSubstance.tsx
  80. 2 0
      apps/app/src/components/Sidebar/InAppNotification/InAppNotification.tsx
  81. 1 1
      apps/app/src/components/Sidebar/PageCreateButton/PageCreateButton.tsx
  82. 4 2
      apps/app/src/components/Sidebar/PageTree/PageTree.tsx
  83. 5 2
      apps/app/src/components/Sidebar/PageTreeItem/Ellipsis.tsx
  84. 2 0
      apps/app/src/components/Sidebar/RecentChanges/RecentChanges.tsx
  85. 0 4
      apps/app/src/components/Sidebar/RecentChanges/RecentChangesSubstance.module.scss
  86. 1 1
      apps/app/src/components/Sidebar/RecentChanges/RecentChangesSubstance.tsx
  87. 2 2
      apps/app/src/components/Sidebar/Sidebar.module.scss
  88. 1 1
      apps/app/src/components/Sidebar/SidebarNav/PrimaryItems.tsx
  89. 2 0
      apps/app/src/components/Sidebar/Tag.tsx
  90. 5 5
      apps/app/src/components/TableOfContents.tsx
  91. 3 3
      apps/app/src/components/TemplateModal/TemplateModal.tsx
  92. 5 2
      apps/app/src/components/User/UserInfo.tsx
  93. 4 5
      apps/app/src/features/external-user-group/client/components/ExternalUserGroup/SyncExecution.tsx
  94. 4 10
      apps/app/src/interfaces/editor-settings.ts
  95. 1 1
      apps/app/src/pages/[[...path]].page.tsx
  96. 5 6
      apps/app/src/pages/forgot-password-errors.page.tsx
  97. 3 4
      apps/app/src/pages/installer.page.tsx
  98. 5 6
      apps/app/src/pages/login/error/[message].page.tsx
  99. 7 8
      apps/app/src/pages/maintenance.page.tsx
  100. 4 5
      apps/app/src/pages/reset-password.page.tsx

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

@@ -391,11 +391,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 resources"
+      "Only duplicate user related pages": "Only duplicate pages you can access"
     },
     "help": {
       "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",
@@ -538,6 +538,7 @@
     "search_again" : "Search again",
     "number_of_list_to_display" : "Display",
     "page_number_unit" : "pages",
+    "hit_number_unit" : "hit",
     "sort_axis": {
       "relationScore": "Sort by relevance",
       "createdAt": "Creation date",
@@ -640,7 +641,7 @@
     "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.",
     "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":{
     "create_bootstrap_4_grid":"Create Bootstrap 4 Grid",

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

@@ -424,11 +424,11 @@
       "Recursively": "再帰的に複製",
       "Duplicate without exist path": "存在するパス以外を複製する",
       "Same page already exists": "同じページがすでに存在します",
-      "Only duplicate user related resources": "ユーザに関連のあるリソースのみを複製する"
+      "Only duplicate user related pages": "自分が閲覧可能なページのみを複製する"
     },
     "help": {
       "recursive": "配下のページも複製します",
-      "only_user_related_resources": "ユーザが閲覧可能なページのみを複製します。また、閲覧権限が「特定グループのみ」で設定されている場合、複製後のページにはユーザが所属するグループのみを閲覧可能なグループとして設定します。"
+      "only_inherit_user_related_groups": "閲覧権限が「特定グループのみ」で設定されている場合、複製されたページを閲覧可能なグループ一覧から、自分が所属していないものは取り除かれます"
     }
   },
   "duplicated_pages": "{{fromPath}} を複製しました",
@@ -571,6 +571,7 @@
     "search_again" : "再検索",
     "number_of_list_to_display" : "表示件数",
     "page_number_unit" : "件",
+    "hit_number_unit" : "件",
     "sort_axis": {
       "relationScore": "関連度順",
       "createdAt": "作成日時",
@@ -673,7 +674,7 @@
     "Username or E-mail has invalid characters": "ユーザー名または、メールアドレスに無効な文字があります",
     "Password minimum character should be more than 6 characters": "パスワードの最小文字数は6文字以上です",
     "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":{
     "create_bootstrap_4_grid":"Bootstrap 4 グリッドを作成",

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

@@ -381,11 +381,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 resources"
+      "Only duplicate user related pages": "Only duplicate pages you can access"
     },
     "help": {
       "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}} 已重复",
@@ -541,6 +541,7 @@
     "search_again" : "再次搜索",
     "number_of_list_to_display" : "显示器的数量",
     "page_number_unit" : "例",
+    "hit_number_unit" : "例",
     "sort_axis": {
       "relationScore": "按相关性排序",
       "createdAt": "按创建日期排序",
@@ -643,7 +644,7 @@
     "Username or E-mail has invalid characters": "用户名或电子邮件有无效的字符",
     "Password minimum character should be more than 6 characters": "密码最小字符应超过6个字符",
     "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":{
     "create_bootstrap_4_grid":"创建Bootstrap 4网格",

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

@@ -124,7 +124,7 @@ export const useUpdateStateAfterSave = (pageId: string|undefined|null, opts?: Up
     await mutateCurrentPageId(pageId);
     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
     // and see: https://github.com/weseek/growi/pull/7118

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

@@ -34,7 +34,7 @@ export const useDrawioModalLauncherForView = (opts?: {
   const { open: openDrawioModal } = useDrawioModal();
 
   const saveByDrawioModal = useCallback(async(drawioMxFile: string, bol: number, eol: number) => {
-    if (currentPage == null || shareLinkId != null) {
+    if (currentPage == null || currentPage.revision == null || shareLinkId != null) {
       return;
     }
 

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

@@ -33,7 +33,7 @@ export const useHandsontableModalLauncherForView = (opts?: {
   const { open: openHandsontableModal } = useHandsontableModal();
 
   const saveByHandsontableModal = useCallback(async(table: MarkdownTable, bol: number, eol: number) => {
-    if (currentPage == null || shareLinkId != null) {
+    if (currentPage == null || currentPage.revision == null || shareLinkId != null) {
       return;
     }
 
@@ -64,6 +64,8 @@ export const useHandsontableModalLauncherForView = (opts?: {
     }
 
     const handler = (bol: number, eol: number) => {
+      if (currentPage.revision == null) return;
+
       const markdown = currentPage.revision.body;
       const currentMarkdownTable = getMarkdownTableFromLine(markdown, bol, eol);
       openHandsontableModal(currentMarkdownTable, false, table => saveByHandsontableModal(table, bol, eol));

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

@@ -1,6 +1,6 @@
 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';
 
@@ -31,7 +31,7 @@ export const deleteBookmarkFolder = async(bookmarkFolderId: string): Promise<voi
 };
 
 // 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 });
 };
 

+ 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>
             {(siteUrl == null || siteUrl === '') && (
               <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>
             )}
@@ -172,7 +171,7 @@ class GitHubSecurityManagementContents extends React.Component {
 
         <div style={{ minHeight: '300px' }}>
           <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>
           </h4>
           <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>
             {(siteUrl == null || siteUrl === '') && (
               <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>
             )}

+ 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>
             {(siteUrl == null || siteUrl === '') && (
               <div className="alert alert-danger">
-                <i
-                  className="icon-exclamation"
+                <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' })}<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>
             )}
@@ -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>
                 {(siteUrl == null || siteUrl === '') && (
                   <div className="alert alert-danger">
-                    <i
-                      className="icon-exclamation"
+                    <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' })}<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>
                 )}

+ 3 - 3
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>
             {(siteUrl == null || siteUrl === '') && (
               <div className="alert alert-danger">
-                <i
-                  className="icon-exclamation"
+                <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' })}<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>
             )}

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

@@ -368,11 +368,11 @@ class SecuritySetting extends React.Component {
             <tbody>
               <tr>
                 <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>
                 <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>
                 <th scope="row">{ t('only_me') }</th>
@@ -444,8 +444,8 @@ class SecuritySetting extends React.Component {
             </div>
             {adminGeneralSecurityContainer.isWikiModeForced && (
               <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
                   dangerouslySetInnerHTML={{
                     __html: t('security_settings.Fixed by env var',

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

@@ -211,7 +211,7 @@ const SlackIntegration = () => {
         <h2 className="admin-setting-header mb-4">
           {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">
-            <i className="icon icon-question ms-1" aria-hidden="true"></i>
+            <span className="material-symbols-outlined ms-1" aria-hidden="true">help</span>
           </a>
         </h2>
 

+ 4 - 5
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 dateFnsFormat from 'date-fns/format';
 import { useTranslation } from 'next-i18next';
 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 = {
@@ -210,7 +209,7 @@ export const UserGroupTable: FC<Props> = ({
                         </button>
                         <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}>
-                            <i className="icon-fw icon-note"></i> {t('Edit')}
+                            <span className="material-symbols-outlined me-1">edit_square</span> {t('Edit')}
                           </button>
                           {onRemove != null
                           && (

+ 3 - 2
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 {
@@ -39,7 +40,7 @@ export const UpdateParentConfirmModal: FC = () => {
                 {t('admin:user_group_management.update_parent_confirm_modal.caution_change_parent', { groupName: targetGroup.name })}
               </div>
               <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')}
               </div>
 

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

@@ -62,7 +62,7 @@ export const UserGroupUserTable = (props: Props): JSX.Element => {
                         type="button"
                         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>
                     </div>
                   </div>

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

@@ -63,7 +63,7 @@ const ExternalAccountTable = (props: ExternalAccountTableProps): JSX.Element =>
                   data-html="true"
                   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>
               </div>
             </th>

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

@@ -30,7 +30,7 @@ const GrantAdminButton = (props: GrantAdminButtonProps): JSX.Element => {
 
   return (
     <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>
   );
 

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

@@ -26,7 +26,7 @@ const GrantReadOnlyButton: React.FC<{
 
   return (
     <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>
   );
 };

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

@@ -33,7 +33,7 @@ const RevokeAdminButton = (props: RevokeAdminButtonProps): JSX.Element => {
   const renderRevokeAdminBtn = () => {
     return (
       <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>
     );
   };
@@ -41,7 +41,7 @@ const RevokeAdminButton = (props: RevokeAdminButtonProps): JSX.Element => {
   const renderRevokeAdminAlert = () => {
     return (
       <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>
       </div>
     );

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

@@ -15,7 +15,7 @@ const RevokeAdminAlert = React.memo((): JSX.Element => {
 
   return (
     <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>
     </div>
   );
@@ -49,7 +49,7 @@ const RevokeAdminMenuItem = (props: Props): JSX.Element => {
   return user.username !== currentUser?.username
     ? (
       <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>
     )
     : <RevokeAdminAlert />;

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

@@ -26,7 +26,7 @@ const RevokeReadOnlyMenuItem: React.FC<{
 
   return (
     <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>
   );
 };

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

@@ -38,7 +38,7 @@ const SendInvitationEmailButton = (props) => {
 
   return (
     <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.send_invitation_email')}</>)}
     </button>

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

@@ -33,7 +33,7 @@ class StatusActivateButton extends React.Component {
 
     return (
       <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>
     );
   }

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

@@ -14,7 +14,7 @@ const SuspendAlert = React.memo((): JSX.Element => {
 
   return (
     <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>
     </div>
   );
@@ -47,7 +47,7 @@ const StatusSuspendMenuItem = (props: Props): JSX.Element => {
   return user.username !== currentUser?.username
     ? (
       <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>
     )
     : <SuspendAlert />;

+ 1 - 1
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>
           <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>
         </li>
       </>

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

@@ -43,7 +43,7 @@ export const BookmarkFolderItemControl: React.FC<{
           onClick={onClickRename}
           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')}
         </DropdownItem>
 
@@ -53,7 +53,7 @@ export const BookmarkFolderItemControl: React.FC<{
           className="pt-2 grw-page-control-dropdown-item text-danger"
           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')}
         </DropdownItem>
       </DropdownMenu>

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

@@ -11,7 +11,7 @@ import {
 } from 'reactstrap';
 
 import { NotAvailableForGuest } from '~/components/NotAvailableForGuest';
-import { IPageOperationProcessData } from '~/interfaces/page-operation';
+import type { IPageOperationProcessData } from '~/interfaces/page-operation';
 import { useSWRxPageInfo } from '~/stores/page';
 import loggerFactory from '~/utils/logger';
 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"
             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')}
           </DropdownItem>
         ) }
@@ -194,7 +194,7 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
             data-testid="open-page-duplicate-modal-btn"
             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')}
           </DropdownItem>
         ) }
@@ -205,7 +205,7 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
             onClick={revertItemClickedHandler}
             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')}
           </DropdownItem>
         ) }
@@ -223,7 +223,7 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
             onClick={pathRecoveryItemClickedHandler}
             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')}
           </DropdownItem>
         ) }
@@ -239,7 +239,7 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
               onClick={deleteItemClickedHandler}
               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')}
             </DropdownItem>
           </>

+ 1 - 1
apps/app/src/components/ContentLinkButtons.tsx

@@ -40,7 +40,7 @@ RecentlyCreatedLinkButton.displayName = 'RecentlyCreatedLinkButton';
 
 
 export type ContentLinkButtonsProps = {
-  author?: IUserHasId,
+  author: IUserHasId | null,
 }
 
 export const ContentLinkButtons = (props: ContentLinkButtonsProps): JSX.Element => {

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

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

@@ -32,10 +32,6 @@ import {
 } from '~/stores/ui';
 
 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 { Skeleton } from '../Skeleton';
 
@@ -80,9 +76,7 @@ const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element
         data-testid="open-presentation-modal-btn"
         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')}
       </DropdownItem>
 
@@ -91,7 +85,7 @@ const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element
         onClick={() => exportAsMarkdown(pageId, revisionId, 'md')}
         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')}
       </DropdownItem>
 
@@ -107,9 +101,7 @@ const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element
         data-testid="open-page-accessories-modal-btn-with-history-tab"
         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')}
       </DropdownItem>
 
@@ -118,9 +110,7 @@ const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element
         data-testid="open-page-accessories-modal-btn-with-attachment-data-tab"
         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')}
       </DropdownItem>
 
@@ -131,9 +121,7 @@ const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element
             data-testid="open-page-accessories-modal-btn-with-share-link-management-data-tab"
             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')}
           </DropdownItem>
         </NotAvailable>
@@ -163,7 +151,7 @@ const CreateTemplateMenuItems = (props: CreateTemplateMenuItemsProps): JSX.Eleme
         className="grw-page-control-dropdown-item"
         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')}
       </DropdownItem>
     </>

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

@@ -68,7 +68,7 @@ export const PageView = (props: Props): JSX.Element => {
   const { data: viewOptions } = useViewOptions();
 
   const page = pageBySWR ?? initialPage;
-  const isNotFound = isNotFoundMeta || page?.revision == null;
+  const isNotFound = isNotFoundMeta || page == null;
   const isUsersHomepagePath = isUsersHomepage(pagePath);
 
   const shouldExpandContent = useShouldExpandContent(page);
@@ -124,7 +124,7 @@ export const PageView = (props: Props): JSX.Element => {
     : null;
 
   const Contents = () => {
-    if (isNotFound) {
+    if (isNotFound || page?.revision == null) {
       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) {
       const isForbidden = error != null && error[0].code === 'forbidden-page';
       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 {
         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'));
       }

+ 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';
 
@@ -40,7 +41,7 @@ const ShareLinkAlert: FC<Props> = (props: Props) => {
 
   return (
     <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>
       // eslint-disable-next-line react/no-danger
         : <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 CustomTabContent from '../CustomNavigation/CustomTabContent';
 import ExpandOrContractButton from '../ExpandOrContractButton';
-import AttachmentIcon from '../Icons/AttachmentIcon';
-import HistoryIcon from '../Icons/HistoryIcon';
-import ShareLinkIcon from '../Icons/ShareLinkIcon';
 
 import { useAutoOpenModalByQueryParam } from './hooks';
 
@@ -46,7 +43,7 @@ export const PageAccessoriesModal = (): JSX.Element => {
   const navTabMapping = useMemo(() => {
     return {
       [PageAccessoriesModalContents.PageHistory]: {
-        Icon: HistoryIcon,
+        Icon: () => <span className="material-symbols-outlined">history</span>,
         Content: () => {
           return <PageHistory onClose={close} />;
         },
@@ -54,14 +51,14 @@ export const PageAccessoriesModal = (): JSX.Element => {
         isLinkEnabled: () => !isGuestUser && !isSharedUser,
       },
       [PageAccessoriesModalContents.Attachment]: {
-        Icon: AttachmentIcon,
+        Icon: () => <span className="material-symbols-outlined">attachment</span>,
         Content: () => {
           return <PageAttachment />;
         },
         i18n: t('attachment_data'),
       },
       [PageAccessoriesModalContents.ShareLink]: {
-        Icon: ShareLinkIcon,
+        Icon: () => <span className="material-symbols-outlined">share</span>,
         Content: () => {
           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 { open: openDeleteAttachmentModal } = useDeleteAttachmentModal();
   const { data: currentPage } = useSWRxCurrentPage();
-  const markdown = currentPage?.revision.body;
+  const markdown = currentPage?.revision?.body;
 
   // Custom hooks
   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 { 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 { 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="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')}
         </div>
         <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">
       <strong>{t('Warning')}: </strong> {t('page_page.notice.version')}
       <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>
     </div>
   );

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

@@ -23,21 +23,21 @@ export const PageGrantAlert = (): JSX.Element => {
       if (pageData.grant === 2) {
         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) {
         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) {
         return (
           <>
-            <i className="icon-fw icon-organization"></i>
+            <span className="material-symbols-outlined me-1">account_tree</span>
             <strong>{
               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 (
     <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>
     </div>
   );

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

@@ -90,7 +90,7 @@ export const TrashPageAlert = (): JSX.Element => {
           onClick={openPutbackPageModalHandler}
           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
           type="button"

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

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

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

+ 5 - 5
apps/app/src/components/PageControls/PageControls.tsx

@@ -107,7 +107,7 @@ type CommonProps = {
 type PageControlsSubstanceProps = CommonProps & {
   pageId: string,
   shareLinkId?: string | null,
-  revisionId: string | null,
+  revisionId?: string | null,
   path?: string | null,
   pageInfo: IPageInfoForOperation,
   expandContentWidth?: boolean,
@@ -178,7 +178,7 @@ const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element =
     const page: IPageToRenameWithMeta = {
       data: {
         _id: pageId,
-        revision: revisionId,
+        revision: revisionId ?? null,
         path,
       },
       meta: pageInfo,
@@ -195,7 +195,7 @@ const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element =
     const pageToDelete: IPageToDeleteWithMeta = {
       data: {
         _id: pageId,
-        revision: revisionId,
+        revision: revisionId ?? null,
         path,
       },
       meta: pageInfo,
@@ -311,7 +311,7 @@ const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element =
 type PageControlsProps = CommonProps & {
   pageId: string,
   shareLinkId?: string | null,
-  revisionId?: string,
+  revisionId?: string | null,
   path?: string | null,
   expandContentWidth?: boolean,
 };
@@ -346,7 +346,7 @@ export const PageControls = memo((props: PageControlsProps): JSX.Element => {
       {...props}
       pageInfo={pageInfo}
       pageId={pageId}
-      revisionId={revisionId ?? null}
+      revisionId={revisionId}
       path={path}
       onClickEditTagsButton={onClickEditTagsButton}
       onClickDuplicateMenuItem={onClickDuplicateMenuItem}

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

@@ -239,8 +239,8 @@ const PageDuplicateModal = (): JSX.Element => {
             onChange={() => setOnlyDuplicateUserRelatedResources(!onlyDuplicateUserRelatedResources)}
           />
           <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>
         </div>
         <div className="mt-3">

+ 6 - 5
apps/app/src/components/PageEditor/Editor.tsx

@@ -1,5 +1,6 @@
+import type { ForwardRefRenderFunction } from 'react';
 import React, {
-  useState, useRef, useImperativeHandle, useCallback, ForwardRefRenderFunction, forwardRef,
+  useState, useRef, useImperativeHandle, useCallback, forwardRef,
   memo,
   useEffect,
 } from 'react';
@@ -11,14 +12,14 @@ import {
 } from 'reactstrap';
 
 import { toastError, toastSuccess } from '~/client/util/toastr';
-import { IEditorSettings } from '~/interfaces/editor-settings';
+import type { IEditorSettings } from '~/interfaces/editor-settings';
 import { useDefaultIndentSize } from '~/stores/context';
 import { useEditorSettings } from '~/stores/editor';
 import { useIsMobile } from '~/stores/ui';
 
-import { IEditorMethods } from '../../interfaces/editor-methods';
+import type { IEditorMethods } from '../../interfaces/editor-methods';
 
-import AbstractEditor from './AbstractEditor';
+import type AbstractEditor from './AbstractEditor';
 import { Cheatsheet } from './Cheatsheet';
 // import CodeMirrorEditor from './CodeMirrorEditor';
 import pasteHelper from './PasteHelper';
@@ -249,7 +250,7 @@ const Editor: ForwardRefRenderFunction<IEditorMethods, EditorPropsType> = (props
     return (
       <Modal isOpen={isCheatsheetModalShown} toggle={hideCheatsheetModal} className={`modal-gfm-cheatsheet ${styles['modal-gfm-cheatsheet']}`} size="lg">
         <ModalHeader tag="h4" toggle={hideCheatsheetModal} className="bg-primary text-light">
-          <i className="icon-fw icon-question" />Markdown help
+          <span className="material-symbols-outlined me-1">help</span>Markdown help
         </ModalHeader>
         <ModalBody>
           <Cheatsheet />

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

@@ -26,7 +26,6 @@ const OptionsSelector = dynamic(() => import('~/components/PageEditor/OptionsSel
 
 const EditorNavbarBottom = (): JSX.Element => {
 
-  const [isExpanded, setExpanded] = useState(false);
   const [isSlackExpanded, setSlackExpanded] = useState(false);
 
   const { data: editorMode } = useEditorMode();
@@ -57,23 +56,8 @@ const EditorNavbarBottom = (): JSX.Element => {
     setSlackChannelsStr(slackChannels);
   }, []);
 
-
-  const renderExpandButton = () => (
-    <div className="d-md-none ms-2">
-      <button
-        type="button"
-        className={`btn btn-outline-secondary btn-expand border-0 ${isExpanded ? 'expand' : ''}`}
-        onClick={() => setExpanded(!isExpanded)}
-      >
-        <i className="icon-arrow-up"></i>
-      </button>
-    </div>
-  );
-
-  const isCollapsedOptionsSelectorEnabled = !isDeviceLargerThanLg;
-
   return (
-    <div className={`${isCollapsedOptionsSelectorEnabled ? 'fixed-bottom' : ''} `} data-testid="grw-editor-navbar-bottom">
+    <div data-testid="grw-editor-navbar-bottom">
       {/* Collapsed SlackNotification */}
       {isSlackConfigured && (
         <Collapse isOpen={isSlackExpanded && !isDeviceLargerThanLg}>
@@ -95,7 +79,7 @@ const EditorNavbarBottom = (): JSX.Element => {
       }
       <div className={`flex-expand-horiz align-items-center px-2 px-md-3 ${moduleClass}`}>
         <form>
-          { isDeviceLargerThanMd && <OptionsSelector /> }
+          <OptionsSelector collapsed={!isDeviceLargerThanMd} />
         </form>
         <form className="row row-cols-lg-auto g-3 align-items-center ms-auto">
           {/* Responsive Design for the SlackNotification */}
@@ -125,21 +109,8 @@ const EditorNavbarBottom = (): JSX.Element => {
             </div>
           ))}
           <SavePageControls slackChannels={slackChannelsStr} />
-          { isCollapsedOptionsSelectorEnabled && renderExpandButton() }
         </form>
       </div>
-      {/* Collapsed OptionsSelector */}
-      { isCollapsedOptionsSelectorEnabled && (
-        <Collapse isOpen={isExpanded}>
-          <div className="px-2"> {/* set padding for border-top */}
-            <div className={`navbar navbar-expand border-top px-0 ${moduleClass}`}>
-              <form className="ms-auto">
-                <OptionsSelector />
-              </form>
-            </div>
-          </div>
-        </Collapse>
-      ) }
     </div>
   );
 };

+ 211 - 170
apps/app/src/components/PageEditor/OptionsSelector.tsx

@@ -2,67 +2,114 @@ import React, {
   memo, useCallback, useMemo, useState,
 } from 'react';
 
+import type {
+  EditorTheme, KeyMapMode,
+} from '@growi/editor';
 import { useTranslation } from 'next-i18next';
+import Image from 'next/image';
 import {
-  Dropdown, DropdownToggle, DropdownMenu, DropdownItem,
+  Dropdown, DropdownToggle, DropdownMenu, Input, FormGroup,
 } from 'reactstrap';
 
 import { useIsIndentSizeForced } from '~/stores/context';
 import { useEditorSettings, useCurrentIndentSize } from '~/stores/editor';
 
-import { DEFAULT_THEME, type KeyMapMode } from '../../interfaces/editor-settings';
+import {
+  DEFAULT_THEME, DEFAULT_KEYMAP,
+} from '../../interfaces/editor-settings';
 
 
-const AVAILABLE_THEMES = [
-  'DefaultLight', 'Eclipse', 'Basic', 'Ayu', 'Rosé Pine', 'DefaultDark', 'Material', 'Nord', 'Cobalt', 'Kimbie',
-];
+type RadioListItemProps = {
+  onClick: () => void,
+  icon?: React.ReactNode,
+  text: string,
+  checked?: boolean
+}
 
-const TYPICAL_INDENT_SIZE = [2, 4];
+const RadioListItem = (props: RadioListItemProps): JSX.Element => {
+  const {
+    onClick, icon, text, checked,
+  } = props;
+  return (
+    <li className="list-group-item border-0 d-flex align-items-center">
+      <input
+        onClick={onClick}
+        className="form-check-input me-3"
+        type="radio"
+        name="listGroupRadio"
+        id={`editor_config_radio_item_${text}`}
+        checked={checked}
+      />
+      {icon}
+      <label className="form-check-label stretched-link fs-6" htmlFor={`editor_config_radio_item_${text}`}>{text}</label>
+    </li>
+  );
+};
+
+
+type SelectorProps = {
+  header: string,
+  onClickBefore: () => void,
+  items: JSX.Element,
+}
 
+const Selector = (props: SelectorProps): JSX.Element => {
+
+  const { header, onClickBefore, items } = props;
+  return (
+    <div className="d-flex flex-column w-100">
+      <button type="button" className="btn border-0 d-flex align-items-center text-muted ms-2" onClick={onClickBefore}>
+        <span className="material-symbols-outlined fs-5 py-0 me-1">navigate_before</span>
+        <label>{header}</label>
+      </button>
+      <hr className="my-1" />
+      <ul className="list-group d-flex ms-2">
+        { items }
+      </ul>
+    </div>
+  );
+
+};
+
+
+type EditorThemeToLabel = {
+  [key in EditorTheme]: string;
+}
 
-const ThemeSelector = (): JSX.Element => {
+const EDITORTHEME_LABEL_MAP: EditorThemeToLabel = {
+  defaultlight: 'DefaultLight',
+  eclipse: 'Eclipse',
+  basic: 'Basic',
+  ayu: 'Ayu',
+  rosepine: 'Rosé Pine',
+  defaultdark: 'DefaultDark',
+  material: 'Material',
+  nord: 'Nord',
+  cobalt: 'Cobalt',
+  kimbie: 'Kimbie',
+};
 
-  const [isThemeMenuOpened, setIsThemeMenuOpened] = useState(false);
+const ThemeSelector = memo(({ onClickBefore }: {onClickBefore: () => void}): JSX.Element => {
 
   const { data: editorSettings, update } = useEditorSettings();
+  const selectedTheme = editorSettings?.theme ?? DEFAULT_THEME;
 
-  const menuItems = useMemo(() => (
+  const listItems = useMemo(() => (
     <>
-      { AVAILABLE_THEMES.map((theme) => {
+      { (Object.keys(EDITORTHEME_LABEL_MAP) as EditorTheme[]).map((theme) => {
+        const themeLabel = EDITORTHEME_LABEL_MAP[theme];
         return (
-          <DropdownItem className="menuitem-label" onClick={() => update({ theme })}>
-            {theme}
-          </DropdownItem>
+          <RadioListItem onClick={() => update({ theme })} text={themeLabel} checked={theme === selectedTheme} />
         );
       }) }
     </>
-  ), [update]);
-
-  const selectedTheme = editorSettings?.theme ?? DEFAULT_THEME;
+  ), [update, selectedTheme]);
 
   return (
-    <div className="input-group flex-nowrap">
-      <div>
-        <span className="input-group-text" id="igt-theme">Theme</span>
-      </div>
-
-      <Dropdown
-        direction="up"
-        isOpen={isThemeMenuOpened}
-        toggle={() => setIsThemeMenuOpened(!isThemeMenuOpened)}
-      >
-        <DropdownToggle color="outline-secondary" caret>
-          {selectedTheme}
-        </DropdownToggle>
-
-        <DropdownMenu container="body">
-          {menuItems}
-        </DropdownMenu>
-
-      </Dropdown>
-    </div>
+    <Selector header="Theme" onClickBefore={onClickBefore} items={listItems} />
   );
-};
+});
+ThemeSelector.displayName = 'ThemeSelector';
 
 
 type KeyMapModeToLabel = {
@@ -76,105 +123,74 @@ const KEYMAP_LABEL_MAP: KeyMapModeToLabel = {
   vscode: 'Visual Studio Code',
 };
 
-const KeymapSelector = memo((): JSX.Element => {
-
-  const [isKeyMenuOpened, setIsKeyMenuOpened] = useState(false);
+const KeymapSelector = memo(({ onClickBefore }: {onClickBefore: () => void}): JSX.Element => {
 
   const { data: editorSettings, update } = useEditorSettings();
+  const selectedKeymapMode = editorSettings?.keymapMode ?? DEFAULT_KEYMAP;
 
-  const menuItems = useMemo(() => (
+  const listItems = useMemo(() => (
     <>
       { (Object.keys(KEYMAP_LABEL_MAP) as KeyMapMode[]).map((keymapMode) => {
         const keymapLabel = KEYMAP_LABEL_MAP[keymapMode];
         const icon = (keymapMode !== 'default')
-          ? <img src={`/images/icons/${keymapMode}.png`} width="16px" className="me-2"></img>
+          ? <Image src={`/images/icons/${keymapMode}.png`} width={16} height={16} className="me-2" alt={keymapMode} />
           : null;
         return (
-          <DropdownItem className="menuitem-label" onClick={() => update({ keymapMode })}>
-            {icon}{keymapLabel}
-          </DropdownItem>
+          <RadioListItem onClick={() => update({ keymapMode })} icon={icon} text={keymapLabel} checked={keymapMode === selectedKeymapMode} />
         );
       }) }
     </>
-  ), [update]);
+  ), [update, selectedKeymapMode]);
 
-  const selectedKeymapMode = editorSettings?.keymapMode ?? 'default';
 
   return (
-    <div className="input-group flex-nowrap">
-      <span className="input-group-text" id="igt-keymap">Keymap</span>
-      <Dropdown
-        direction="up"
-        isOpen={isKeyMenuOpened}
-        toggle={() => setIsKeyMenuOpened(!isKeyMenuOpened)}
-      >
-        <DropdownToggle color="outline-secondary" caret>
-          {selectedKeymapMode}
-        </DropdownToggle>
-
-        <DropdownMenu container="body">
-          {menuItems}
-        </DropdownMenu>
-
-      </Dropdown>
-    </div>
+    <Selector header="Keymap" onClickBefore={onClickBefore} items={listItems} />
   );
-
 });
-
 KeymapSelector.displayName = 'KeymapSelector';
 
-type IndentSizeSelectorProps = {
-  isIndentSizeForced: boolean,
-  selectedIndentSize: number,
-  onChange: (indentSize: number) => void,
-}
 
-const IndentSizeSelector = memo(({ isIndentSizeForced, selectedIndentSize, onChange }: IndentSizeSelectorProps): JSX.Element => {
+const TYPICAL_INDENT_SIZE = [2, 4];
+
+const IndentSizeSelector = memo(({ onClickBefore }: {onClickBefore: () => void}): JSX.Element => {
 
-  const [isIndentMenuOpened, setIsIndentMenuOpened] = useState(false);
+  const { data: currentIndentSize, mutate: mutateCurrentIndentSize } = useCurrentIndentSize();
 
-  const menuItems = useMemo(() => (
+  const listItems = useMemo(() => (
     <>
       { TYPICAL_INDENT_SIZE.map((indent) => {
         return (
-          <DropdownItem className="menuitem-label" onClick={() => onChange(indent)}>
-            {indent}
-          </DropdownItem>
+          <RadioListItem onClick={() => mutateCurrentIndentSize(indent)} text={indent.toString()} checked={indent === currentIndentSize} />
         );
       }) }
     </>
-  ), [onChange]);
+  ), [currentIndentSize, mutateCurrentIndentSize]);
 
   return (
-    <div className="input-group flex-nowrap">
-      <span className="input-group-text" id="igt-indent">Indent</span>
-      <Dropdown
-        direction="up"
-        isOpen={isIndentMenuOpened}
-        toggle={() => setIsIndentMenuOpened(!isIndentMenuOpened)}
-        disabled={isIndentSizeForced}
-      >
-        <DropdownToggle color="outline-secondary" caret>
-          {selectedIndentSize}
-        </DropdownToggle>
-
-        <DropdownMenu container="body">
-          {menuItems}
-        </DropdownMenu>
-
-      </Dropdown>
-    </div>
+    <Selector header="Indent" onClickBefore={onClickBefore} items={listItems} />
   );
 });
-
 IndentSizeSelector.displayName = 'IndentSizeSelector';
 
 
-const ConfigurationDropdown = memo((): JSX.Element => {
-  const { t } = useTranslation();
+type SwitchItemProps = {
+  onClick: () => void,
+  checked: boolean,
+  text: string,
+};
+const SwitchItem = memo((props: SwitchItemProps): JSX.Element => {
+  const { onClick, checked, text } = props;
+  return (
+    <FormGroup switch>
+      <Input type="switch" checked={checked} onClick={onClick} />
+      <label>{text}</label>
+    </FormGroup>
 
-  const [isCddMenuOpened, setCddMenuOpened] = useState(false);
+  );
+});
+
+const ConfigurationSelector = memo((): JSX.Element => {
+  const { t } = useTranslation();
 
   const { data: editorSettings, update } = useEditorSettings();
 
@@ -185,20 +201,8 @@ const ConfigurationDropdown = memo((): JSX.Element => {
 
     const isActive = editorSettings.styleActiveLine;
 
-    const iconClasses = ['text-info'];
-    if (isActive) {
-      iconClasses.push('ti ti-check');
-    }
-    const iconClassName = iconClasses.join(' ');
-
     return (
-      <DropdownItem toggle={false} onClick={() => update({ styleActiveLine: !isActive })}>
-        <div className="d-flex justify-content-between">
-          <span className="icon-container"></span>
-          <span className="menuitem-label">{ t('page_edit.Show active line') }</span>
-          <span className="icon-container"><i className={iconClassName}></i></span>
-        </div>
-      </DropdownItem>
+      <SwitchItem onClick={() => update({ styleActiveLine: !isActive })} checked={isActive} text={t('page_edit.Show active line')} />
     );
   }, [editorSettings, update, t]);
 
@@ -209,81 +213,118 @@ const ConfigurationDropdown = memo((): JSX.Element => {
 
     const isActive = editorSettings.autoFormatMarkdownTable;
 
-    const iconClasses = ['text-info'];
-    if (isActive) {
-      iconClasses.push('ti ti-check');
-    }
-    const iconClassName = iconClasses.join(' ');
-
     return (
-      <DropdownItem toggle={false} onClick={() => update({ autoFormatMarkdownTable: !isActive })}>
-        <div className="d-flex justify-content-between">
-          <span className="icon-container"></span>
-          <span className="menuitem-label">{ t('page_edit.auto_format_table') }</span>
-          <span className="icon-container"><i className={iconClassName}></i></span>
-        </div>
-      </DropdownItem>
+      <SwitchItem onClick={() => update({ autoFormatMarkdownTable: !isActive })} checked={isActive} text={t('page_edit.auto_format_table')} />
     );
   }, [editorSettings, t, update]);
 
   return (
-    <div className="my-0">
-      <Dropdown
-        direction="up"
-        className="grw-editor-configuration-dropdown"
-        isOpen={isCddMenuOpened}
-        toggle={() => setCddMenuOpened(!isCddMenuOpened)}
-      >
-
-        <DropdownToggle color="outline-secondary" caret>
-          <i className="icon-settings"></i>
-        </DropdownToggle>
-
-        <DropdownMenu container="body">
-          {renderActiveLineMenuItem()}
-          {renderMarkdownTableAutoFormattingMenuItem()}
-          {/* <DropdownItem divider /> */}
-        </DropdownMenu>
-
-      </Dropdown>
+    <div className="mx-3 mt-1">
+      {renderActiveLineMenuItem()}
+      {renderMarkdownTableAutoFormattingMenuItem()}
     </div>
   );
+});
+ConfigurationSelector.displayName = 'ConfigurationSelector';
+
 
+type ChangeStateButtonProps = {
+  onClick: () => void,
+  header: string,
+  data: string,
+  disabled?: boolean,
+}
+const ChangeStateButton = memo((props: ChangeStateButtonProps): JSX.Element => {
+  const {
+    onClick, header, data, disabled,
+  } = props;
+  return (
+    <button type="button" className="d-flex align-items-center btn btn-sm border-0 my-1" disabled={disabled} onClick={onClick}>
+      <label className="ms-2 me-auto">{header}</label>
+      <label className="text-muted d-flex align-items-center ms-2 me-1">
+        {data}
+        <span className="material-symbols-outlined fs-5 py-0">navigate_next</span>
+      </label>
+    </button>
+  );
 });
 
-ConfigurationDropdown.displayName = 'ConfigurationDropdown';
 
+const OptionsStatus = {
+  Home: 'Home',
+  Theme: 'Theme',
+  Keymap: 'Keymap',
+  Indent: 'Indent',
+} as const;
+type OptionStatus = typeof OptionsStatus[keyof typeof OptionsStatus];
+
+export const OptionsSelector = ({ collapsed }: {collapsed?: boolean}): JSX.Element => {
 
-export const OptionsSelector = (): JSX.Element => {
+  const [dropdownOpen, setDropdownOpen] = useState(false);
+
+  const [status, setStatus] = useState<OptionStatus>(OptionsStatus.Home);
   const { data: editorSettings } = useEditorSettings();
+  const { data: currentIndentSize } = useCurrentIndentSize();
   const { data: isIndentSizeForced } = useIsIndentSizeForced();
-  const { data: currentIndentSize, mutate: mutateCurrentIndentSize } = useCurrentIndentSize();
 
-  if (editorSettings == null || isIndentSizeForced == null || currentIndentSize == null) {
+  if (editorSettings == null || currentIndentSize == null || isIndentSizeForced == null) {
     return <></>;
   }
 
   return (
-    <>
-      <div className="d-flex flex-row zindex-dropdown">
-        <span>
-          <ThemeSelector />
-        </span>
-        <span className="d-none d-sm-block ms-2 ms-sm-4">
-          <KeymapSelector />
-        </span>
-        <span className="ms-2 ms-sm-4">
-          <IndentSizeSelector
-            isIndentSizeForced={isIndentSizeForced}
-            selectedIndentSize={currentIndentSize}
-            onChange={newValue => mutateCurrentIndentSize(newValue)}
-          />
-        </span>
-        <span className="ms-2 ms-sm-4">
-          <ConfigurationDropdown />
-        </span>
-      </div>
-    </>
+    <Dropdown isOpen={dropdownOpen} toggle={() => { setStatus(OptionsStatus.Home); setDropdownOpen(!dropdownOpen) }} direction="up" className="">
+      <DropdownToggle
+        className={`btn btn-outline-neutral-secondary d-flex align-items-center justify-content-center p-1 m-1
+              ${collapsed ? 'border-0' : 'border border-secondary'}
+              ${dropdownOpen ? 'active' : ''}
+              `}
+      >
+        <span className="material-symbols-outlined py-0 fs-5"> settings </span>
+        {
+          collapsed ? <></>
+            : <label className="ms-1 me-1">Editor Config</label>
+        }
+      </DropdownToggle>
+      <DropdownMenu container="body">
+        {
+          status === OptionsStatus.Home && (
+            <div className="d-flex flex-column">
+              <label className="text-muted ms-3">
+                Editor Config
+              </label>
+              <hr className="my-1" />
+              <ChangeStateButton onClick={() => setStatus(OptionsStatus.Theme)} header="Theme" data={EDITORTHEME_LABEL_MAP[editorSettings.theme ?? ''] ?? ''} />
+              <hr className="my-1" />
+              <ChangeStateButton
+                onClick={() => setStatus(OptionsStatus.Keymap)}
+                header="Keymap"
+                data={KEYMAP_LABEL_MAP[editorSettings.keymapMode ?? ''] ?? ''}
+              />
+              <hr className="my-1" />
+              <ChangeStateButton
+                disabled={isIndentSizeForced}
+                onClick={() => setStatus(OptionsStatus.Indent)}
+                header="Indent"
+                data={currentIndentSize.toString() ?? ''}
+              />
+              <hr className="my-1" />
+              <ConfigurationSelector />
+            </div>
+          )
+        }
+        { status === OptionsStatus.Theme && (
+          <ThemeSelector onClickBefore={() => setStatus(OptionsStatus.Home)} />
+        )
+        }
+        { status === OptionsStatus.Keymap && (
+          <KeymapSelector onClickBefore={() => setStatus(OptionsStatus.Home)} />
+        )
+        }
+        { status === OptionsStatus.Indent && (
+          <IndentSizeSelector onClickBefore={() => setStatus(OptionsStatus.Home)} />
+        )
+        }
+      </DropdownMenu>
+    </Dropdown>
   );
-
 };

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

@@ -133,13 +133,7 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
   const { resolvedTheme } = useNextThemes();
   mutateResolvedTheme({ themeData: resolvedTheme });
 
-  // TODO: remove workaround
-  // for https://redmine.weseek.co.jp/issues/125923
-  const [createdPageRevisionIdWithAttachment, setCreatedPageRevisionIdWithAttachment] = useState();
-
-  // TODO: remove workaround
-  // for https://redmine.weseek.co.jp/issues/125923
-  const currentRevisionId = currentPage?.revision?._id ?? createdPageRevisionIdWithAttachment;
+  const currentRevisionId = currentPage?.revision?._id;
 
   const initialValueRef = useRef('');
   const initialValue = useMemo(() => {
@@ -194,12 +188,6 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
 
   }, [markdownToPreview, mutateIsConflict]);
 
-  // TODO: remove workaround
-  // for https://redmine.weseek.co.jp/issues/125923
-  useEffect(() => {
-    setCreatedPageRevisionIdWithAttachment(undefined);
-  }, [router]);
-
   useEffect(() => {
     if (socket == null) { return }
 
@@ -359,7 +347,7 @@ export const PageEditor = React.memo((props: Props): JSX.Element => {
     mutateIsConflict(false);
 
     // set resolved markdown in editing markdown
-    const markdown = pageData?.revision.body ?? '';
+    const markdown = pageData?.revision?.body ?? '';
     mutateEditingMarkdown(markdown);
 
   }, [mutateCurrentPage, mutateEditingMarkdown, mutateIsConflict, mutateTagsInfo, syncTagsInfoForEditor]);

+ 1 - 1
apps/app/src/components/PageEditor/page-path-rename-utils.ts

@@ -40,7 +40,7 @@ export const usePagePathRenameHandler = (
     try {
       await apiv3Put('/pages/rename', {
         pageId: currentPage._id,
-        revisionId: currentPage.revision._id,
+        revisionId: currentPage.revision?._id,
         newPagePath,
       });
 

+ 1 - 1
apps/app/src/components/PageHistory/Revision.tsx

@@ -75,7 +75,7 @@ export const Revision = (props: RevisionProps): JSX.Element => {
               onClick={onClose}
               prefetch={false}
             >
-              <i className="icon-login"></i> {t('Go to this version')}
+              <span className="material-symbols-outlined">login</span> {t('Go to this version')}
             </Link>
           </div>
         </div>

+ 2 - 2
apps/app/src/components/PageHistory/RevisionDiff.tsx

@@ -61,7 +61,7 @@ export const RevisionDiff = (props: RevisioinDiffProps): JSX.Element => {
                 onClick={onClose}
                 prefetch={false}
               >
-                <i className="icon-login"></i>
+                <span className="material-symbols-outlined">login</span>
               </Link>
             </div>
             <div className="col comparison-target-wrapper pt-1">
@@ -72,7 +72,7 @@ export const RevisionDiff = (props: RevisioinDiffProps): JSX.Element => {
                 onClick={onClose}
                 prefetch={false}
               >
-                <i className="icon-login"></i>
+                <span className="material-symbols-outlined">login</span>
               </Link>
             </div>
           </div>

+ 9 - 7
apps/app/src/components/PageList/PageListItemL.tsx

@@ -1,6 +1,6 @@
+import type { ForwardRefRenderFunction } from 'react';
 import React, {
-  forwardRef, useState,
-  ForwardRefRenderFunction, memo, useCallback, useImperativeHandle, useRef, useEffect,
+  forwardRef, useState, memo, useCallback, useImperativeHandle, useRef, useEffect,
 } from 'react';
 
 import type {
@@ -16,11 +16,12 @@ import Link from 'next/link';
 import Clamp from 'react-multiline-clamp';
 import { Input } from 'reactstrap';
 
-import { ISelectable } from '~/client/interfaces/selectable-all';
+import type { ISelectable } from '~/client/interfaces/selectable-all';
 import { unlink, bookmark, unbookmark } from '~/client/services/page-operation';
 import { toastError } from '~/client/util/toastr';
-import { IPageSearchMeta, IPageWithSearchMeta, isIPageSearchMeta } from '~/interfaces/search';
-import {
+import type { IPageSearchMeta, IPageWithSearchMeta } from '~/interfaces/search';
+import { isIPageSearchMeta } from '~/interfaces/search';
+import type {
   OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction, OnPutBackedFunction,
 } from '~/interfaces/ui';
 import LinkedPagePath from '~/models/linked-page-path';
@@ -31,7 +32,8 @@ import {
 import { useIsDeviceLargerThanLg } from '~/stores/ui';
 
 import { useSWRMUTxPageInfo, useSWRxPageInfo } from '../../stores/page';
-import { ForceHideMenuItems, PageItemControl } from '../Common/Dropdown/PageItemControl';
+import type { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
+import { PageItemControl } from '../Common/Dropdown/PageItemControl';
 import { PagePathHierarchicalLink } from '../Common/PagePathHierarchicalLink';
 
 type Props = {
@@ -286,7 +288,7 @@ const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (pr
                 {
                   !hasBrowsingRights && (
                     <>
-                      <i className="icon-exclamation p-1"></i>
+                      <span className="material-symbols-outlined p-1">error</span>
                       {t('not_allowed_to_see_this_page')}
                     </>
                   )

+ 9 - 9
apps/app/src/components/PageManagement/ApiErrorMessage.jsx

@@ -18,26 +18,26 @@ const ApiErrorMessage = (props) => {
       case 'already_exists':
         return (
           <>
-            <strong><i className="icon-fw icon-ban"></i>{ t('page_api_error.already_exists') }</strong>
-            <small><a href={targetPath}>{targetPath} <i className="icon-login"></i></a></small>
+            <strong><span className="material-symbols-outlined me-1">cancel</span>{ t('page_api_error.already_exists') }</strong>
+            <small><a href={targetPath}>{targetPath} <span className="material-symbols-outlined me-1">login</span></a></small>
           </>
         );
       case 'notfound_or_forbidden':
         return (
-          <strong><i className="icon-fw icon-ban"></i>{ t('page_api_error.notfound_or_forbidden') }</strong>
+          <strong><span className="material-symbols-outlined me-1">cancel</span>{ t('page_api_error.notfound_or_forbidden') }</strong>
         );
       case 'user_not_admin':
         return (
-          <strong><i className="icon-fw icon-ban"></i>{ t('page_api_error.user_not_admin') }</strong>
+          <strong><span className="material-symbols-outlined me-1">cancel</span>{ t('page_api_error.user_not_admin') }</strong>
         );
       case 'complete_deletion_not_allowed_for_user':
         return (
-          <strong><i className="icon-fw icon-ban"></i>{ t('page_api_error.complete_deletion_not_allowed_for_user') }</strong>
+          <strong><span className="material-symbols-outlined me-1">cancel</span>{ t('page_api_error.complete_deletion_not_allowed_for_user') }</strong>
         );
       case 'outdated':
         return (
           <>
-            <strong><i className="icon-fw icon-bulb"></i> { t('page_api_error.outdated') }</strong>
+            <strong><span className="material-symbols-outlined me-1">lightbulb</span> { t('page_api_error.outdated') }</strong>
             <a className="btn-link" onClick={reload}>
               <i className="fa fa-angle-double-right"></i> { t('Load latest') }
             </a>
@@ -45,15 +45,15 @@ const ApiErrorMessage = (props) => {
         );
       case 'invalid_path':
         return (
-          <strong><i className="icon-fw icon-ban"></i> Invalid path</strong>
+          <strong><span className="material-symbols-outlined me-1">cancel</span> Invalid path</strong>
         );
       case 'single_deletion_empty_pages':
         return (
-          <strong><i className="icon-fw icon-ban"></i>{ t('page_api_error.single_deletion_empty_pages') }</strong>
+          <strong><span className="material-symbols-outlined me-1">cancel</span>{ t('page_api_error.single_deletion_empty_pages') }</strong>
         );
       default:
         return (
-          <strong><i className="icon-fw icon-ban"></i> Unknown error occured</strong>
+          <strong><span className="material-symbols-outlined me-1">cancel</span> Unknown error occured</strong>
         );
     }
   }

+ 1 - 1
apps/app/src/components/PagePresentationModal.tsx

@@ -60,7 +60,7 @@ const PagePresentationModal = (): JSX.Element => {
     return <></>;
   }
 
-  const markdown = currentPage?.revision.body;
+  const markdown = currentPage?.revision?.body;
 
   return (
     <Modal

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

@@ -1,6 +1,7 @@
 import React, { Suspense, useCallback } from 'react';
 
-import { getIdForRef, type IPageHasId, type IPageInfoForOperation } from '@growi/core';
+import type { IPagePopulatedToShowRevision } from '@growi/core';
+import { getIdForRef, type IPageInfoForOperation } from '@growi/core';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
@@ -70,7 +71,7 @@ const Tags = (props: TagsProps): JSX.Element => {
 
 
 export type PageSideContentsProps = {
-  page: IPageHasId,
+  page: IPagePopulatedToShowRevision,
   isSharedUser?: boolean,
 }
 
@@ -91,9 +92,11 @@ export const PageSideContents = (props: PageSideContentsProps): JSX.Element => {
   return (
     <>
       {/* Tags */}
-      <Suspense fallback={<PageTagsSkeleton />}>
-        <Tags pageId={page._id} revisionId={getIdForRef(page.revision)} />
-      </Suspense>
+      { page.revision != null && (
+        <Suspense fallback={<PageTagsSkeleton />}>
+          <Tags pageId={page._id} revisionId={page.revision._id} />
+        </Suspense>
+      ) }
 
       <div className={`${styles['grw-page-accessories-controls']} d-flex flex-column gap-2`}>
         {/* Page list */}

+ 1 - 1
apps/app/src/components/PageStatusAlert.tsx

@@ -38,7 +38,7 @@ export const PageStatusAlert = (): JSX.Element => {
 
   const refreshPage = useCallback(async() => {
     const updatedPageData = await mutatePageData();
-    mutateEditingMarkdown(updatedPageData?.revision.body);
+    mutateEditingMarkdown(updatedPageData?.revision?.body);
   }, [mutateEditingMarkdown, mutatePageData]);
 
   const onClickResolveConflict = useCallback(() => {

+ 1 - 1
apps/app/src/components/PageTimeline.tsx

@@ -30,7 +30,7 @@ const TimelineCard = ({ page }: TimelineCardProps): JSX.Element => {
         </Link>
       </div>
       <div className="card-body">
-        { rendererOptions != null && (
+        { rendererOptions != null && page.revision != null && (
           <RevisionLoader
             rendererOptions={rendererOptions}
             pageId={page._id}

+ 2 - 2
apps/app/src/components/ReactMarkdownComponents/DrawioViewerWithEditButton.tsx

@@ -1,6 +1,6 @@
 import React, { useCallback, useState } from 'react';
 
-import EventEmitter from 'events';
+import type EventEmitter from 'events';
 
 import {
   DrawioViewer,
@@ -65,7 +65,7 @@ export const DrawioViewerWithEditButton = React.memo((props: DrawioViewerProps):
           className="btn btn-outline-secondary btn-edit-drawio"
           onClick={editButtonClickHandler}
         >
-          <i className="icon-note me-1"></i>{t('Edit')}
+          <span className="material-symbols-outlined me-1">edit_square</span>{t('Edit')}
         </button>
       ) }
       <DrawioViewer {...props} onRenderingStart={renderingStartHandler} onRenderingUpdated={renderingUpdatedHandler} />

+ 4 - 4
apps/app/src/components/ReactMarkdownComponents/Header.tsx

@@ -1,9 +1,9 @@
 import { useCallback, useEffect, useState } from 'react';
 
-import EventEmitter from 'events';
+import type EventEmitter from 'events';
 
 import { useRouter } from 'next/router';
-import { Element } from 'react-markdown/lib/rehype-filter';
+import type { Element } from 'react-markdown/lib/rehype-filter';
 
 import {
   useIsGuestUser, useIsReadOnlyUser, useIsSharedUser, useShareLinkId,
@@ -42,7 +42,7 @@ const EditLink = (props: EditLinkProps): JSX.Element => {
   return (
     <span className="revision-head-edit-button">
       <a href="#edit" aria-disabled={isDisabled} onClick={() => setCaretLine(props.line)}>
-        <i className="icon-note"></i>
+        <span className="material-symbols-outlined">edit_square</span>
       </a>
     </span>
   );
@@ -116,7 +116,7 @@ export const Header = (props: HeaderProps): JSX.Element => {
     <CustomTag id={id} className={`revision-head ${styles['revision-head']} ${isActive ? 'blink' : ''}`}>
       {children}
       <NextLink href={`#${id}`} className="revision-head-link">
-        <span className="icon-link"></span>
+        <span className="material-symbols-outlined">link</span>
       </NextLink>
       {showEditButton && (
         <EditLink line={node.position?.start.line} />

+ 1 - 1
apps/app/src/components/ReactMarkdownComponents/RichAttachment.tsx

@@ -67,7 +67,7 @@ export const RichAttachment = React.memo((props: RichAttachmentProps) => {
                 {attachmentName || originalName}
               </a>
               <a className="ms-2 attachment-download" href={downloadPathProxied}>
-                <i className="icon-cloud-download" />
+                <span className="material-symbols-outlined">cloud_download</span>
               </a>
               <a className="ml-2 text-danger attachment-delete d-share-link-none" type="button" onClick={onClickTrashButtonHandler}>
                 <span className="material-symbols-outlined">delete</span>

+ 3 - 3
apps/app/src/components/ReactMarkdownComponents/TableWithEditButton.tsx

@@ -1,8 +1,8 @@
 import React, { useCallback } from 'react';
 
-import EventEmitter from 'events';
+import type EventEmitter from 'events';
 
-import { Element } from 'react-markdown/lib/rehype-filter';
+import type { Element } from 'react-markdown/lib/rehype-filter';
 
 import {
   useIsGuestUser, useIsReadOnlyUser, useIsSharedUser, useShareLinkId,
@@ -43,7 +43,7 @@ export const TableWithEditButton = React.memo((props: TableWithEditButtonProps):
     <div className={`editable-with-handsontable ${styles['editable-with-handsontable']}`}>
       { showEditButton && (
         <button type="button" className="handsontable-modal-trigger" onClick={editButtonClickHandler}>
-          <i className="icon-note"></i>
+          <span className="material-symbols-outlined">edit_square</span>
         </button>
       )}
       <table className={className}>

+ 2 - 2
apps/app/src/components/SavePageControls/GrantSelector/GrantSelector.tsx

@@ -137,7 +137,7 @@ export const GrantSelector = (props: Props): JSX.Element => {
     if (userRelatedGrantedGroups != null && userRelatedGrantedGroups.length > 0) {
       const labelElm = (
         <span>
-          <i className="icon icon-fw icon-organization"></i>
+          <span className="material-symbols-outlined me-1">account_tree</span>
           <span className="label">
             {userRelatedGrantedGroups.length > 1
               ? (
@@ -192,7 +192,7 @@ export const GrantSelector = (props: Props): JSX.Element => {
         <div>
           <h4>{t('user_group.belonging_to_no_group')}</h4>
           { currentUser?.admin && (
-            <p><a href="/admin/user-groups"><i className="icon icon-fw icon-login"></i>{t('user_group.manage_user_groups')}</a></p>
+            <p><a href="/admin/user-groups"><span className="material-symbols-outlined me-1">login</span>{t('user_group.manage_user_groups')}</a></p>
           ) }
         </div>
       );

+ 8 - 17
apps/app/src/components/SearchPage.tsx

@@ -28,8 +28,6 @@ const INITIAL_PAGIONG_SIZE = 20;
 
 type SearchResultListHeadProps = {
   searchResult: IFormattedSearchResult,
-  searchingKeyword: string,
-  offset: number,
   pagingSize: number,
   onPagingSizeChanged: (size: number) => void,
 }
@@ -38,13 +36,10 @@ const SearchResultListHead = React.memo((props: SearchResultListHeadProps): JSX.
   const { t } = useTranslation();
 
   const {
-    searchResult, searchingKeyword, offset, pagingSize,
-    onPagingSizeChanged,
+    searchResult, // pagingSize, onPagingSizeChanged,
   } = props;
 
-  const { took, total, hitsCount } = searchResult.meta;
-  const leftNum = offset + 1;
-  const rightNum = offset + hitsCount;
+  const { took, total } = searchResult.meta;
 
   if (total === 0) {
     return (
@@ -57,15 +52,14 @@ const SearchResultListHead = React.memo((props: SearchResultListHeadProps): JSX.
   return (
     <div className="d-flex align-items-center justify-content-between">
       <div className="text-nowrap">
-        {t('search_result.result_meta')}
-        <span className="search-result-keyword ms-2">{`${searchingKeyword}`}</span>
-        <span className="ms-3">{`${leftNum}-${rightNum}`} / {total}</span>
+        <span className="ms-3 fw-bold">{total} {t('search_result.hit_number_unit', 'hit')}</span>
         { took != null && (
           // blackout 70px rectangle in VRT
           <span data-vrt-blackout className="ms-3 text-muted d-inline-block" style={{ minWidth: '70px' }}>({took}ms)</span>
         ) }
       </div>
-      <div className="input-group flex-nowrap search-result-select-group ms-auto d-md-flex d-none">
+      {/* TODO: infinite scroll for search result */}
+      {/* <div className="input-group flex-nowrap search-result-select-group ms-auto d-md-flex d-none">
         <div>
           <label className="form-label input-group-text text-muted" htmlFor="inputGroupSelect01">{t('search_result.number_of_list_to_display')}</label>
         </div>
@@ -79,7 +73,7 @@ const SearchResultListHead = React.memo((props: SearchResultListHeadProps): JSX.
             return <option key={limit} value={limit}>{limit} {t('search_result.page_number_unit')}</option>;
           })}
         </select>
-      </div>
+      </div> */}
     </div>
   );
 });
@@ -185,11 +179,10 @@ export const SearchPage = (): JSX.Element => {
           >
             <button
               type="button"
-              className="btn btn-outline-danger text-nowrap border-0 px-2"
+              className="btn border-0 text-danger"
               disabled={isDisabled}
               onClick={deleteAllButtonClickedHandler}
             >
-              <span className="material-symbols-outlined">delete</span>
               {t('search_result.delete_all_selected_page')}
             </button>
           </OperateAllControl>
@@ -221,13 +214,11 @@ export const SearchPage = (): JSX.Element => {
     return (
       <SearchResultListHead
         searchResult={data}
-        searchingKeyword={keyword ?? ''}
-        offset={offset}
         pagingSize={limit}
         onPagingSizeChanged={pagingSizeChangedHandler}
       />
     );
-  }, [data, keyword, limit, offset, pagingSizeChangedHandler]);
+  }, [data, limit, pagingSizeChangedHandler]);
 
   const searchPager = useMemo(() => {
     // when pager is not needed

+ 5 - 5
apps/app/src/components/SearchPage/OperateAllControl.tsx

@@ -1,11 +1,10 @@
-import React, {
-  ChangeEvent, forwardRef, ForwardRefRenderFunction, useImperativeHandle, useRef,
-} from 'react';
+import type { ChangeEvent, ForwardRefRenderFunction } from 'react';
+import React, { forwardRef, useImperativeHandle, useRef } from 'react';
 
 import { Input } from 'reactstrap';
 
-import { ISelectableAndIndeterminatable } from '~/client/interfaces/selectable-all';
-import { IndeterminateInputElement } from '~/interfaces/indeterminate-input-elm';
+import type { ISelectableAndIndeterminatable } from '~/client/interfaces/selectable-all';
+import type { IndeterminateInputElement } from '~/interfaces/indeterminate-input-elm';
 
 type Props = {
   isCheckboxDisabled?: boolean,
@@ -58,6 +57,7 @@ const OperateAllControlSubstance: ForwardRefRenderFunction<ISelectableAndIndeter
         type="checkbox"
         id="cb-check-all"
         data-testid="cb-select-all"
+        className="ms-2"
         innerRef={selectAllCheckboxElm}
         disabled={isCheckboxDisabled}
         onChange={checkboxChangedHandler}

+ 9 - 0
apps/app/src/components/SearchPage/SearchControl.module.scss

@@ -0,0 +1,9 @@
+
+@use '@growi/core/scss/bootstrap/init' as bs;
+
+@use '@growi/ui/scss/atoms/btn-muted';
+
+// == Colors
+.btn-delete {
+  @include btn-muted.colorize(bs.$red);
+}

+ 24 - 22
apps/app/src/components/SearchPage/SearchControl.tsx

@@ -5,13 +5,15 @@ import React, {
 import { useTranslation } from 'next-i18next';
 
 import { SORT_AXIS, SORT_ORDER } from '~/interfaces/search';
-import { ISearchConditions, ISearchConfigurations } from '~/stores/search';
+import type { ISearchConditions, ISearchConfigurations } from '~/stores/search';
 
 import SearchForm from '../SearchForm';
 
 import SearchOptionModal from './SearchOptionModal';
 import SortControl from './SortControl';
 
+import styles from './SearchControl.module.scss';
+
 type Props = {
   isSearchServiceReachable: boolean,
   isEnableSort: boolean,
@@ -93,26 +95,12 @@ const SearchControl = React.memo((props: Props): JSX.Element => {
             onSubmit={searchFormSubmittedHandler}
           />
         </div>
-
-        {/* sort option: show when screen is larger than lg */}
-        {isEnableSort && (
-          <div className="me-4 d-lg-flex d-none">
-            <SortControl
-              sort={sort}
-              order={order}
-              onChange={changeSortHandler}
-            />
-          </div>
-        )}
       </div>
       {/* TODO: replace the following elements deleteAll button , relevance button and include specificPath button component */}
       <div className="search-control d-flex align-items-center py-md-2 py-3 px-md-4 px-3 border-bottom border-gray">
-        <div className="d-flex">
-          {allControl}
-        </div>
-        {/* sort option: show when screen is smaller than lg */}
+        {/* sort option */}
         {isEnableSort && (
-          <div className="me-md-4 me-2 d-flex d-lg-none ms-auto">
+          <div className="flex-grow-1">
             <SortControl
               sort={sort}
               order={order}
@@ -129,11 +117,13 @@ const SearchControl = React.memo((props: Props): JSX.Element => {
                 className="btn"
                 onClick={() => setIsFileterOptionModalShown(true)}
               >
-                <i className="icon-equalizer"></i>
+                <span className="material-symbols-outlined">
+                  tune
+                </span>
               </button>
             </div>
-            <div className="d-none d-lg-flex align-items-center ms-auto search-control-include-options">
-              <div className="border rounded px-2 py-1 me-3">
+            <div className="d-none d-lg-flex align-items-center search-control-include-options">
+              <div className="px-2 py-1">
                 <div className="form-check form-check-succsess">
                   <input
                     className="form-check-input me-2"
@@ -150,7 +140,7 @@ const SearchControl = React.memo((props: Props): JSX.Element => {
                   </label>
                 </div>
               </div>
-              <div className="border rounded px-2 py-1">
+              <div className="px-2 py-1">
                 <div className="form-check form-check-succsess">
                   <input
                     className="form-check-input me-2"
@@ -160,7 +150,7 @@ const SearchControl = React.memo((props: Props): JSX.Element => {
                     onChange={e => changeIncludeTrashPagesHandler(e.target.checked)}
                   />
                   <label
-                    className="form-label form-check-label d-flex align-items-center text-secondary with-no-font-weight"
+                    className="form-label form-check-label mb-0 d-flex align-items-center text-secondary with-no-font-weight"
                     htmlFor="flexCheckChecked"
                   >
                     {t('Include Subordinated Target Page', { target: '/trash' })}
@@ -170,6 +160,18 @@ const SearchControl = React.memo((props: Props): JSX.Element => {
             </div>
           </>
         )}
+        <div className="d-flex">
+          <div className="btn-group">
+            {/* TODO: imprv to delete all result UI */}
+            {/* <button className={`btn btn-sm rounded ${styles['btn-delete']}`} type="button" data-bs-toggle="dropdown" aria-expanded="false">
+              <span className="material-symbols-outlined ">delete</span>
+              <span className="material-symbols-outlined ">expand_more</span>
+            </button> */}
+            {/* <ul className="dropdown-menu"> */}
+            {allControl}
+            {/* </ul> */}
+          </div>
+        </div>
       </div>
 
       <SearchOptionModal

+ 4 - 3
apps/app/src/components/SearchPage/SearchOptionModal.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 {
@@ -51,7 +52,7 @@ const SearchOptionModal: FC<Props> = (props: Props) => {
       </ModalHeader>
       <ModalBody>
         <div className="d-flex p-2">
-          <div className="border border-gray me-3">
+          <div className="me-3">
             <label className="form-label px-3 py-2 mb-0 d-flex align-items-center">
               <input
                 className="me-2"
@@ -62,7 +63,7 @@ const SearchOptionModal: FC<Props> = (props: Props) => {
               {t('Include Subordinated Target Page', { target: '/user' })}
             </label>
           </div>
-          <div className="border border-gray">
+          <div className="">
             <label className="form-label px-3 py-2 mb-0 d-flex align-items-center">
               <input
                 className="me-2"

+ 18 - 19
apps/app/src/components/SearchPage/SearchResultContent.tsx

@@ -1,5 +1,6 @@
+import type { FC } from 'react';
 import React, {
-  FC, useCallback, useEffect, useRef, useState,
+  useCallback, useEffect, useRef, useState,
 } from 'react';
 
 import { getIdForRef } from '@growi/core';
@@ -56,7 +57,7 @@ const AdditionalMenuItems = (props: AdditionalMenuItemsProps): JSX.Element => {
       onClick={() => exportAsMarkdown(pageId, revisionId, 'md')}
       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')}
     </DropdownItem>
   );
@@ -118,7 +119,7 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
 
   const { t } = useTranslation();
 
-  const page = pageWithMeta?.data;
+  const page = pageWithMeta.data;
   const { open: openDuplicateModal } = usePageDuplicateModal();
   const { open: openRenameModal } = usePageRenameModal();
   const { open: openDeleteModal } = usePageDeleteModal();
@@ -182,7 +183,10 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
       return <></>;
     }
 
-    const revisionId = getIdForRef(page.revision);
+    const revisionId = page.revision != null ? getIdForRef(page.revision) : null;
+    const additionalMenuItemRenderer = revisionId != null
+      ? props => <AdditionalMenuItems {...props} pageId={page._id} revisionId={revisionId} />
+      : undefined;
 
     return (
       <div className="d-flex flex-column align-items-end justify-content-center px-2 py-1">
@@ -193,7 +197,7 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
           expandContentWidth={shouldExpandContent}
           showPageControlDropdown={showPageControlDropdown}
           forceHideMenuItems={forceHideMenuItems}
-          additionalMenuItemRenderer={props => <AdditionalMenuItems {...props} pageId={page._id} revisionId={revisionId} />}
+          additionalMenuItemRenderer={additionalMenuItemRenderer}
           onClickDuplicateMenuItem={duplicateItemClickedHandler}
           onClickRenameMenuItem={renameItemClickedHandler}
           onClickDeleteMenuItem={deleteItemClickedHandler}
@@ -204,8 +208,6 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
   }, [page, shouldExpandContent, showPageControlDropdown, forceHideMenuItems,
       duplicateItemClickedHandler, renameItemClickedHandler, deleteItemClickedHandler, switchContentWidthHandler]);
 
-  const isRenderable = page != null && rendererOptions != null;
-
   const fluidLayoutClass = shouldExpandContent ? _fluidLayoutClass : '';
 
   return (
@@ -216,25 +218,23 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
     >
       <RightComponent />
 
-      { isRenderable && (
-        <div className="container-lg grw-container-convertible pt-2 pb-2">
-          <PagePathNav pageId={page._id} pagePath={page.path} formerLinkClassName="small" latterLinkClassName="fs-3" />
-        </div>
-      ) }
+      <div className="container-lg grw-container-convertible pt-2 pb-2">
+        <PagePathNav pageId={page._id} pagePath={page.path} formerLinkClassName="small" latterLinkClassName="fs-3" />
+      </div>
 
       <div
         id="search-result-content-body-container"
         ref={scrollElementRef}
         className="search-result-content-body-container container-lg grw-container-convertible overflow-y-scroll"
       >
-        { isRenderable && (
+        { page.revision != null && rendererOptions != null && (
           <RevisionLoader
             rendererOptions={rendererOptions}
             pageId={page._id}
             revisionId={page.revision}
           />
         )}
-        { isRenderable && (
+        { page.revision != null && (
           <PageComment
             rendererOptions={rendererOptions}
             pageId={page._id}
@@ -244,11 +244,10 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
             isReadOnly
           />
         )}
-        { isRenderable && (
-          <PageContentFooter
-            page={page}
-          />
-        )}
+
+        <PageContentFooter
+          page={page}
+        />
       </div>
     </div>
   );

+ 3 - 0
apps/app/src/components/SearchPage/SortControl.module.scss

@@ -0,0 +1,3 @@
+.sort-control {
+  min-width: 180px;
+}

+ 32 - 35
apps/app/src/components/SearchPage/SortControl.tsx

@@ -1,7 +1,12 @@
-import React, { FC } from 'react';
+import type { FC } from 'react';
+import React from 'react';
+
 import { useTranslation } from 'next-i18next';
+
 import { SORT_AXIS, SORT_ORDER } from '../../interfaces/search';
 
+import styles from './SortControl.module.scss';
+
 const { DESC, ASC } = SORT_ORDER;
 
 type Props = {
@@ -22,43 +27,35 @@ const SortControl: FC <Props> = (props: Props) => {
     }
   };
 
-  const renderOrderIcon = () => {
-    const iconClassName = ASC === order ? 'fa fa-sort-amount-asc' : 'fa fa-sort-amount-desc';
-    return <i className={iconClassName} aria-hidden="true" />;
-  };
 
   return (
     <>
-      <div className="input-group flex-nowrap">
-        <div>
-          <div className="input-group-text border text-muted" id="btnGroupAddon">
-            {renderOrderIcon()}
-          </div>
-        </div>
-        <div className="border rounded-end">
-          <button
-            type="button"
-            className="btn dropdown-toggle py-1"
-            data-bs-toggle="dropdown"
-          >
-            <span className="me-2 text-secondary">{t(`search_result.sort_axis.${sort}`)}</span>
-          </button>
-          <div className="dropdown-menu dropdown-menu-right">
-            {Object.values(SORT_AXIS).map((sortAxis) => {
-              const nextOrder = (sort !== sortAxis || order === ASC) ? DESC : ASC;
-              return (
-                <button
-                  key={sortAxis}
-                  className="dropdown-item"
-                  type="button"
-                  onClick={() => { onClickChangeSort(sortAxis, nextOrder) }}
-                >
-                  <span>{t(`search_result.sort_axis.${sortAxis}`)}</span>
-                </button>
-              );
-            })}
-          </div>
-        </div>
+      <div className={`btn-group ${styles['sort-control']}`}>
+        <button
+          className="d-flex align-items-center btn btn-sm btn-outline-neutral-secondary rounded-pill"
+          type="button"
+          data-bs-toggle="dropdown"
+          aria-expanded="false"
+        >
+          <span className="material-symbols-outlined py-0">sort</span>
+          <span className="ms-2 me-auto">{t(`search_result.sort_axis.${sort}`)}</span>
+          <span className="material-symbols-outlined py-0">expand_more</span>
+        </button>
+        <ul className="dropdown-menu">
+          {Object.values(SORT_AXIS).map((sortAxis) => {
+            const nextOrder = (sort !== sortAxis || order === ASC) ? DESC : ASC;
+            return (
+              <button
+                key={sortAxis}
+                className="dropdown-item"
+                type="button"
+                onClick={() => { onClickChangeSort(sortAxis, nextOrder) }}
+              >
+                <span>{t(`search_result.sort_axis.${sortAxis}`)}</span>
+              </button>
+            );
+          })}
+        </ul>
       </div>
     </>
   );

+ 1 - 1
apps/app/src/components/ShareLinkPageView.tsx

@@ -67,7 +67,7 @@ export const ShareLinkPageView = (props: Props): JSX.Element => {
 
 
   const Contents = () => {
-    if (isNotFound) {
+    if (isNotFound || page.revision == null) {
       return <></>;
     }
 

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

@@ -13,6 +13,8 @@ export const Bookmarks = () : JSX.Element => {
 
   return (
     <>
+      {/* TODO : #139425 Match the space specification method to others */}
+      {/* ref.  https://redmine.weseek.co.jp/issues/139425 */}
       <div className="grw-sidebar-content-header p-3">
         <h3 className="mb-0">{t('Bookmarks')}</h3>
       </div>

+ 8 - 5
apps/app/src/components/Sidebar/Bookmarks/BookmarkContents.tsx

@@ -38,15 +38,18 @@ export const BookmarkContents = (): JSX.Element => {
   }, [mutateBookmarkFolders]);
 
   return (
-    <>
-      <div className="col-8 mb-2 ">
+    <div className="ms-3">
+      <div className="col-8 mb-2">
         <button
           type="button"
           className="btn btn-outline-secondary rounded-pill d-flex justify-content-start align-middle"
           onClick={onClickNewBookmarkFolder}
         >
-          <FolderPlusIcon />
-          <span className="mx-2 ">{t('bookmark_folder.new_folder')}</span>
+
+          <div className="d-flex align-items-center">
+            <FolderPlusIcon />
+            <span className="ms-2">{t('bookmark_folder.new_folder')}</span>
+          </div>
         </button>
       </div>
       {isCreateAction && (
@@ -58,6 +61,6 @@ export const BookmarkContents = (): JSX.Element => {
         </div>
       )}
       <BookmarkFolderTree isOperable userId={currentUser?._id} />
-    </>
+    </div>
   );
 };

+ 2 - 0
apps/app/src/components/Sidebar/Custom/CustomSidebar.tsx

@@ -18,6 +18,8 @@ export const CustomSidebar = (): JSX.Element => {
   const { mutate, isLoading } = useSWRxPageByPath('/Sidebar');
 
   return (
+    // TODO : #139425 Match the space specification method to others
+    // ref.  https://redmine.weseek.co.jp/issues/139425
     <div className="px-3">
       <div className="grw-sidebar-content-header py-3 d-flex">
         <h3 className="mb-0">

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

@@ -19,7 +19,7 @@ export const CustomSidebarSubstance = (): JSX.Element => {
 
   if (rendererOptions == null) return <></>;
 
-  const markdown = page?.revision.body;
+  const markdown = page?.revision?.body;
 
   return (
     <div className={`py-3 grw-custom-sidebar-content ${styles['grw-custom-sidebar-content']}`}>

+ 2 - 0
apps/app/src/components/Sidebar/InAppNotification/InAppNotification.tsx

@@ -15,6 +15,8 @@ export const InAppNotification = (): JSX.Element => {
   const [isUnopendNotificationsVisible, setUnopendNotificationsVisible] = useState(false);
 
   return (
+    // TODO : #139425 Match the space specification method to others
+    // ref.  https://redmine.weseek.co.jp/issues/139425
     <div className="px-3">
       <div className="grw-sidebar-content-header py-3 d-flex">
         <h3 className="mb-0">

+ 1 - 1
apps/app/src/components/Sidebar/PageCreateButton/PageCreateButton.tsx

@@ -57,7 +57,7 @@ export const PageCreateButton = React.memo((): JSX.Element => {
 
   return (
     <div
-      className="d-flex flex-row"
+      className="d-flex flex-row mt-2"
       onMouseEnter={onMouseEnterHandler}
       onMouseLeave={onMouseLeaveHandler}
     >

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

@@ -16,8 +16,10 @@ export const PageTree = (): JSX.Element => {
   const { t } = useTranslation();
 
   return (
-    <div className="px-3">
-      <div className="grw-sidebar-content-header py-3 d-flex">
+    // TODO : #139425 Match the space specification method to others
+    // ref.  https://redmine.weseek.co.jp/issues/139425
+    <div className="pt-4 pb-3 px-3">
+      <div className="grw-sidebar-content-header d-flex">
         <h3 className="mb-0">{t('Page Tree')}</h3>
         <Suspense>
           <PageTreeHeader />

+ 5 - 2
apps/app/src/components/Sidebar/PageTreeItem/Ellipsis.tsx

@@ -1,5 +1,6 @@
+import type { FC } from 'react';
 import React, {
-  useCallback, useState, FC,
+  useCallback, useState,
 } from 'react';
 
 import nodePath from 'path';
@@ -125,10 +126,12 @@ export const Ellipsis: FC<TreeItemToolProps> = (props) => {
     }
   };
 
+  const hasChildren = page.descendantCount ? page.descendantCount > 0 : false;
+
   return (
     <>
       {isRenameInputShown ? (
-        <div className="flex-fill">
+        <div className={`position-absolute ${hasChildren ? 'ms-5' : 'ms-4'}`}>
           <NotDraggableForClosableTextInput>
             <ClosableTextInput
               value={nodePath.basename(page.path ?? '')}

+ 2 - 0
apps/app/src/components/Sidebar/RecentChanges/RecentChanges.tsx

@@ -18,6 +18,8 @@ export const RecentChanges = (): JSX.Element => {
   const [isSmall, setIsSmall] = useState(false);
 
   return (
+    // TODO : #139425 Match the space specification method to others
+    // ref.  https://redmine.weseek.co.jp/issues/139425
     <div className="px-3" data-testid="grw-recent-changes">
       <div className="grw-sidebar-content-header py-3 d-flex">
         <h3 className="mb-0 text-nowrap">{t('Recent Changes')}</h3>

+ 0 - 4
apps/app/src/components/Sidebar/RecentChanges/RecentChangesSubstance.module.scss

@@ -34,10 +34,6 @@
     width: 80px;
   }
 
-  .icon-lock {
-    font-size: 14px;
-  }
-
   // For truncate-text
   .flex-grow-1 {
     min-width: 0;

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

@@ -98,7 +98,7 @@ const PageItem = memo(({ page, isSmall, onClickTag }: PageItemProps): JSX.Elemen
 
   let locked;
   if (page.grant !== 1) {
-    locked = <span><i className="icon-lock ms-2" /></span>;
+    locked = <span className="material-symbols-outlined ms-2">lock</span>;
   }
 
   const isTagElementsRendered = !(isSmall || (page.tags.length === 0));

+ 2 - 2
apps/app/src/components/Sidebar/Sidebar.module.scss

@@ -86,7 +86,7 @@
     &:global {
       &.grw-sidebar-collapsed {
         .sidebar-contents-container {
-          background-color: rgba(var(--grw-highlight-100-rgb), .5);
+          background-color: rgba(var(--grw-highlight-100-rgb), .8);
           backdrop-filter: blur(20px);
         }
       }
@@ -108,7 +108,7 @@
     &:global {
       &.grw-sidebar-collapsed {
         .sidebar-contents-container {
-          background-color: rgba(var(--grw-highlight-800-rgb), .5);
+          background-color: rgba(var(--grw-highlight-800-rgb), .8);
           backdrop-filter: blur(20px);
         }
       }

+ 1 - 1
apps/app/src/components/Sidebar/SidebarNav/PrimaryItems.tsx

@@ -29,7 +29,7 @@ export const PrimaryItems = memo((props: Props) => {
 
   return (
     <div className={styles['grw-primary-items']}>
-      <PrimaryItem sidebarMode={sidebarMode} contents={SidebarContentsType.TREE} label="Page Tree" iconName="format_list_bulleted" onHover={onItemHover} />
+      <PrimaryItem sidebarMode={sidebarMode} contents={SidebarContentsType.TREE} label="Page Tree" iconName="list" onHover={onItemHover} />
       <PrimaryItem sidebarMode={sidebarMode} contents={SidebarContentsType.CUSTOM} label="Custom Sidebar" iconName="code" onHover={onItemHover} />
       <PrimaryItem sidebarMode={sidebarMode} contents={SidebarContentsType.RECENT} label="Recent Changes" iconName="update" onHover={onItemHover} />
       <PrimaryItem sidebarMode={sidebarMode} contents={SidebarContentsType.BOOKMARKS} label="Bookmarks" iconName="bookmarks" onHover={onItemHover} />

+ 2 - 0
apps/app/src/components/Sidebar/Tag.tsx

@@ -44,6 +44,8 @@ const Tag: FC = () => {
 
   // todo: adjust design by XD
   return (
+    // TODO : #139425 Match the space specification method to others
+    // ref.  https://redmine.weseek.co.jp/issues/139425
     <div className="container-lg px-4 mb-5 pb-5" data-testid="grw-sidebar-content-tags">
       <div className="grw-sidebar-content-header py-3 d-flex">
         <h3 className="mb-0">{t('Tags')}</h3>

+ 5 - 5
apps/app/src/components/TableOfContents.tsx

@@ -11,7 +11,7 @@ import { StickyStretchableScroller } from './StickyStretchableScroller';
 
 import styles from './TableOfContents.module.scss';
 
-const { isUserPage: _isUserPage } = pagePathUtils;
+const { isUsersHomepage: _isUsersHomepage } = pagePathUtils;
 
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
 const logger = loggerFactory('growi:TableOfContents');
@@ -19,7 +19,7 @@ const logger = loggerFactory('growi:TableOfContents');
 const TableOfContents = (): JSX.Element => {
   const { data: currentPagePath } = useCurrentPagePath();
 
-  const isUserPage = currentPagePath != null && _isUserPage(currentPagePath);
+  const isUsersHomePage = currentPagePath != null && _isUsersHomepage(currentPagePath);
 
   const { data: rendererOptions } = useTocOptions();
 
@@ -41,13 +41,13 @@ const TableOfContents = (): JSX.Element => {
     // get smaller bottom line of window height - .system-version height - margin 5px) and containerTop
     let bottom = Math.min(window.innerHeight - 20 - 5, parentBottom);
 
-    if (isUserPage) {
+    if (isUsersHomePage) {
       // raise the bottom line by the height and margin-top of UserContentLinks
-      bottom -= 45;
+      bottom -= 90;
     }
     // bottom - revisionToc top
     return bottom - (containerTop + containerPaddingTop);
-  }, [isUserPage, rendererOptions]);
+  }, [isUsersHomePage, rendererOptions]);
 
   return (
     <div id="revision-toc" className={`revision-toc ${styles['revision-toc']}`}>

+ 3 - 3
apps/app/src/components/TemplateModal/TemplateModal.tsx

@@ -4,7 +4,7 @@ import React, {
 
 import assert from 'assert';
 
-import { Lang } from '@growi/core';
+import type { Lang } from '@growi/core';
 import { useTemplateModal, type TemplateModalStatus } from '@growi/editor/src/stores/use-template-modal';
 import {
   extractSupportedLocales, getLocalizedTemplate, type TemplateSummary,
@@ -70,7 +70,7 @@ const TemplateListGroupItem: React.FC<TemplateSummaryItemProps> = ({
     >
       <h4 className="mb-1 d-flex">
         <span className="d-inline-block text-truncate">{localizedTemplate.title}</span>
-        {localizedTemplate.pluginId != null ? <i className="icon-fw icon-puzzle ms-2 text-muted small"></i> : ''}
+        {localizedTemplate.pluginId != null ? <span className="material-symbols-outlined me-1 ms-2 text-muted small">extension</span> : ''}
       </h4>
       <p className="mb-2">{localizedTemplate.desc}</p>
       { templateLocales != null && Array.from(templateLocales).map(locale => (
@@ -99,7 +99,7 @@ const TemplateDropdownItem: React.FC<TemplateSummaryItemProps> = ({
     >
       <h4 className="mb-1 d-flex">
         <span className="d-inline-block text-truncate">{localizedTemplate.title}</span>
-        {localizedTemplate.pluginId != null ? <i className="icon-fw icon-puzzle ms-2 text-muted small"></i> : ''}
+        {localizedTemplate.pluginId != null ? <span className="material-symbols-outlined me-1 ms-2 text-muted small">extension</span> : ''}
       </h4>
       <p className="mb-1 text-wrap">{localizedTemplate.desc}</p>
       { templateLocales != null && Array.from(templateLocales).map(locale => (

+ 5 - 2
apps/app/src/components/User/UserInfo.tsx

@@ -26,9 +26,12 @@ export const UserInfo = (props: UserInfoProps): JSX.Element => {
           {author.name}
         </h1>
         <div className="user-page-meta mt-3 mb-0">
-          <span className="user-page-username me-4"><i className="icon-user me-1"></i>{author.username}</span>
+          <span className="user-page-username me-4">
+            {/* TODO:Replace with Material Symbols Outlined */}
+            <span className="user-page-username me-4"><i className="icon-user me-1"></i>{author.username}</span>
+          </span>
           <span className="user-page-email me-2">
-            <i className="icon-envelope me-1"></i>
+            <span className="material-symbols-outlined me-1">mail</span>
             { author.isEmailPublished
               ? author.email
               : '*****'

+ 4 - 5
apps/app/src/features/external-user-group/client/components/ExternalUserGroup/SyncExecution.tsx

@@ -1,6 +1,5 @@
-import {
-  FC, useCallback, useEffect, useState,
-} from 'react';
+import type { FC } from 'react';
+import { useCallback, useEffect, useState } from 'react';
 
 import { useTranslation } from 'react-i18next';
 import { Modal, ModalHeader, ModalBody } from 'reactstrap';
@@ -8,7 +7,7 @@ import { Modal, ModalHeader, ModalBody } from 'reactstrap';
 import { apiv3Get } from '~/client/util/apiv3-client';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import LabeledProgressBar from '~/components/Admin/Common/LabeledProgressBar';
-import { ExternalGroupProviderType } from '~/features/external-user-group/interfaces/external-user-group';
+import type { ExternalGroupProviderType } from '~/features/external-user-group/interfaces/external-user-group';
 import { SocketEventName } from '~/interfaces/websocket';
 import { useAdminSocket } from '~/stores/socket-io';
 
@@ -153,7 +152,7 @@ export const SyncExecution = ({
         toggle={() => setIsAlertModalOpen(false)}
       >
         <ModalHeader tag="h4" toggle={() => setIsAlertModalOpen(false)} className="bg-purple text-light">
-          <i className="icon-fw icon-exclamation align-middle"></i>
+          <span className="material-symbols-outlined me-1 align-middle">error</span>
           <span className="align-middle">{t('external_user_group.confirmation_before_sync')}</span>
         </ModalHeader>
         <ModalBody>

+ 4 - 10
apps/app/src/interfaces/editor-settings.ts

@@ -1,16 +1,10 @@
-export const DEFAULT_THEME = 'DefaultLight';
+import { type EditorTheme, type KeyMapMode } from '@growi/editor';
 
-const KeyMapMode = {
-  default: 'default',
-  vim: 'vim',
-  emacs: 'emacs',
-  vscode: 'vscode',
-} as const;
-
-export type KeyMapMode = typeof KeyMapMode[keyof typeof KeyMapMode];
+export const DEFAULT_KEYMAP = 'default';
+export const DEFAULT_THEME = 'defaultlight';
 
 export interface IEditorSettings {
-  theme: undefined | string,
+  theme: undefined | EditorTheme,
   keymapMode: undefined | KeyMapMode,
   styleActiveLine: boolean,
   autoFormatMarkdownTable: boolean,

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

@@ -260,7 +260,7 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
     if (currentPageId != null && !props.isNotFound) {
       const mutatePageData = async() => {
         const pageData = await mutateCurrentPage();
-        mutateEditingMarkdown(pageData?.revision.body);
+        mutateEditingMarkdown(pageData?.revision?.body);
       };
 
       // If skipSSR is true, use the API to retrieve page data.

+ 5 - 6
apps/app/src/pages/forgot-password-errors.page.tsx

@@ -1,15 +1,14 @@
 import React from 'react';
 
-import { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
+import type { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
 import { useTranslation } from 'next-i18next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import Link from 'next/link';
 
 import { forgotPasswordErrorCode } from '~/interfaces/errors/forgot-password';
 
-import {
-  CommonProps, getNextI18NextConfig, getServerSideCommonProps,
-} from './utils/commons';
+import type { CommonProps } from './utils/commons';
+import { getNextI18NextConfig, getServerSideCommonProps } from './utils/commons';
 
 type Props = CommonProps & {
   errorCode?: forgotPasswordErrorCode
@@ -26,7 +25,7 @@ const ForgotPasswordErrorsPage: NextPage<Props> = (props: Props) => {
           <div className="row justify-content-md-center">
             <div className="col-md-6 mt-5">
               <div className="text-center">
-                <h1><i className="icon-lock-open large" /></h1>
+                <h1><span className="material-symbols-outlined large">lock_open</span></h1>
                 <h2 className="text-center">{ t('forgot_password.reset_password') }</h2>
 
                 { errorCode == null && (
@@ -43,7 +42,7 @@ const ForgotPasswordErrorsPage: NextPage<Props> = (props: Props) => {
                       <h2>{ t('forgot_password.incorrect_token_or_expired_url') }</h2>
                     </div>
                     <Link href="/forgot-password" className="link-switch" prefetch={false}>
-                      <i className="icon-key"></i> { t('forgot_password.forgot_password') }
+                      <span className="material-symbols-outlined">key</span> { t('forgot_password.forgot_password') }
                     </Link>
                   </div>
                 ) }

+ 3 - 4
apps/app/src/pages/installer.page.tsx

@@ -1,7 +1,7 @@
 import React, { useMemo } from 'react';
 
 import { pagePathUtils } from '@growi/core/dist/utils';
-import {
+import type {
   NextPage, GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
 import { useTranslation } from 'next-i18next';
@@ -16,9 +16,8 @@ import {
   useCsrfToken, useAppTitle, useSiteUrl, useConfidential,
 } from '../stores/context';
 
-import {
-  CommonProps, getNextI18NextConfig, getServerSideCommonProps, generateCustomTitle,
-} from './utils/commons';
+import type { CommonProps } from './utils/commons';
+import { getNextI18NextConfig, getServerSideCommonProps, generateCustomTitle } from './utils/commons';
 
 
 const DataTransferForm = dynamic(() => import('../components/DataTransferForm'), { ssr: false });

+ 5 - 6
apps/app/src/pages/login/error/[message].page.tsx

@@ -1,6 +1,6 @@
 import React from 'react';
 
-import {
+import type {
   NextPage, GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
 import { useTranslation } from 'next-i18next';
@@ -8,9 +8,8 @@ import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import { useRouter } from 'next/router';
 
 import { NoLoginLayout } from '~/components/Layout/NoLoginLayout';
-import {
-  CommonProps, getServerSideCommonProps, getNextI18NextConfig,
-} from '~/pages/utils/commons';
+import type { CommonProps } from '~/pages/utils/commons';
+import { getServerSideCommonProps, getNextI18NextConfig } from '~/pages/utils/commons';
 
 
 type Props = CommonProps;
@@ -54,7 +53,7 @@ const LoginPage: NextPage<CommonProps> = () => {
           <h2>{ t('forgot_password.incorrect_token_or_expired_url') }</h2>
         </div>
         <a href="/forgot-password" className="link-switch">
-          <i className="icon-key"></i> { t('forgot_password.forgot_password') }
+          <span className="material-symbols-outlined">key</span> { t('forgot_password.forgot_password') }
         </a>
       </>
     );
@@ -92,7 +91,7 @@ const LoginPage: NextPage<CommonProps> = () => {
           </div>
           {/* If the transition source is "/login", use <a /> tag since the transition will not occur if next/link is used. */}
           <a href="/login">
-            <i className="icon-login me-1" />{t('Sign in is here')}
+            <span className="material-symbols-outlined me-1">login</span>{t('Sign in is here')}
           </a>
         </div>
       </div>

+ 7 - 8
apps/app/src/pages/maintenance.page.tsx

@@ -1,5 +1,5 @@
 import type { IUser, IUserHasId } from '@growi/core';
-import { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
+import type { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
 import { useTranslation } from 'next-i18next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 
@@ -8,9 +8,8 @@ import { toastError } from '~/client/util/toastr';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import { useCurrentUser } from '~/stores/context';
 
-import {
-  CommonProps, getServerSideCommonProps, getNextI18NextConfig,
-} from './utils/commons';
+import type { CommonProps } from './utils/commons';
+import { getServerSideCommonProps, getNextI18NextConfig } from './utils/commons';
 
 
 type Props = CommonProps & {
@@ -38,7 +37,7 @@ const MaintenancePage: NextPage<CommonProps> = (props: Props) => {
         <div className="row justify-content-md-center">
           <div className="col-md-6 mt-5">
             <div className="text-center">
-              <h1><i className="icon-exclamation large"></i></h1>
+              <h1><span className="material-symbols-outlined large">error</span></h1>
               <h1 className="text-center">{ t('maintenance_mode.maintenance_mode') }</h1>
               <h3>{ t('maintenance_mode.growi_is_under_maintenance') }</h3>
               <hr />
@@ -46,20 +45,20 @@ const MaintenancePage: NextPage<CommonProps> = (props: Props) => {
                 {props.currentUser?.admin
               && (
                 <p>
-                  <i className="icon-arrow-right"></i>
+                  <span className="material-symbols-outlined">arrow_circle_right</span>
                   <a className="btn btn-link" href="/admin">{ t('maintenance_mode.admin_page') }</a>
                 </p>
               )}
                 {props.currentUser != null
                   ? (
                     <p>
-                      <i className="icon-arrow-right"></i>
+                      <span className="material-symbols-outlined">arrow_circle_right</span>
                       <a className="btn btn-link" onClick={logoutHandler} id="maintanounse-mode-logout">{ t('maintenance_mode.logout') }</a>
                     </p>
                   )
                   : (
                     <p>
-                      <i className="icon-arrow-right"></i>
+                      <span className="material-symbols-outlined">arrow_circle_right</span>
                       <a className="btn btn-link" href="/login">{ t('maintenance_mode.login') }</a>
                     </p>
                   )

+ 4 - 5
apps/app/src/pages/reset-password.page.tsx

@@ -1,13 +1,12 @@
 import React from 'react';
 
-import { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
+import type { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
 import { useTranslation } from 'next-i18next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import dynamic from 'next/dynamic';
 
-import {
-  CommonProps, getNextI18NextConfig, getServerSideCommonProps,
-} from './utils/commons';
+import type { CommonProps } from './utils/commons';
+import { getNextI18NextConfig, getServerSideCommonProps } from './utils/commons';
 
 
 type Props = CommonProps & {
@@ -26,7 +25,7 @@ const ForgotPasswordPage: NextPage<Props> = (props: Props) => {
           <div className="row justify-content-md-center">
             <div className="col-md-6 mt-5">
               <div className="text-center">
-                <h1><i className="icon-lock-open large"></i></h1>
+                <h1><span className="material-symbols-outlined large">lock_open</span></h1>
                 <h2 className="text-center">{ t('forgot_password.reset_password') }</h2>
                 <h5>{ props.email }</h5>
                 <p className="mt-4">{ t('forgot_password.password_reset_excecution_desc') }</p>

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