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

Merge branch 'dev/7.0.x' into feat/yjs-editor

Yuki Takei 2 лет назад
Родитель
Сommit
e44ca1e63c
100 измененных файлов с 960 добавлено и 490 удалено
  1. 81 0
      .github/workflows/release-rc-v7.yml
  2. 3 3
      .vscode/settings.json
  3. 53 1
      CHANGELOG.md
  4. 1 0
      apps/app/.eslintrc.js
  5. 2 2
      apps/app/_obsolete/src/components/Navbar/GrowiNavbar.tsx
  6. 1 1
      apps/app/_obsolete/src/components/PageEditor/CodeMirrorEditor.jsx
  7. 4 4
      apps/app/_obsolete/src/components/PageEditor/ConflictDiffModal.tsx
  8. 15 13
      apps/app/_obsolete/src/components/PageEditor/MarkdownTableInterceptor.js
  9. 9 9
      apps/app/_obsolete/src/components/PageEditorByHackmd.tsx
  10. 2 1
      apps/app/docker/README.md
  11. 11 3
      apps/app/package.json
  12. 4 3
      apps/app/public/static/locales/en_US/admin.json
  13. 7 1
      apps/app/public/static/locales/en_US/commons.json
  14. 19 0
      apps/app/public/static/locales/en_US/translation.json
  15. 4 3
      apps/app/public/static/locales/ja_JP/admin.json
  16. 7 1
      apps/app/public/static/locales/ja_JP/commons.json
  17. 19 0
      apps/app/public/static/locales/ja_JP/translation.json
  18. 5 4
      apps/app/public/static/locales/zh_CN/admin.json
  19. 7 1
      apps/app/public/static/locales/zh_CN/commons.json
  20. 19 0
      apps/app/public/static/locales/zh_CN/translation.json
  21. 10 0
      apps/app/src/client/services/AdminGeneralSecurityContainer.js
  22. 2 2
      apps/app/src/client/services/side-effects/drawio-modal-launcher-for-view.ts
  23. 4 4
      apps/app/src/client/services/side-effects/handsontable-modal-launcher-for-view.ts
  24. 15 9
      apps/app/src/client/services/use-on-template-button-clicked.ts
  25. 6 0
      apps/app/src/client/services/user-ui-settings.ts
  26. 2 2
      apps/app/src/components/Admin/App/ConfirmModal.tsx
  27. 3 3
      apps/app/src/components/Admin/App/FileUploadSetting.tsx
  28. 1 1
      apps/app/src/components/Admin/App/MailSetting.tsx
  29. 1 1
      apps/app/src/components/Admin/App/MaintenanceMode.tsx
  30. 2 2
      apps/app/src/components/Admin/App/QuestionnaireSettings.tsx
  31. 1 1
      apps/app/src/components/Admin/App/SiteUrlSetting.tsx
  32. 1 1
      apps/app/src/components/Admin/App/V5PageMigration.tsx
  33. 2 1
      apps/app/src/components/Admin/AuditLog/AuditLogDisableMode.tsx
  34. 3 3
      apps/app/src/components/Admin/AuditLog/AuditLogSettings.tsx
  35. 1 1
      apps/app/src/components/Admin/AuditLog/SearchUsernameTypeahead.tsx
  36. 3 3
      apps/app/src/components/Admin/AuditLogManagement.tsx
  37. 17 17
      apps/app/src/components/Admin/Common/AdminNavigation.tsx
  38. 2 2
      apps/app/src/components/Admin/ElasticsearchManagement/StatusTable.jsx
  39. 3 3
      apps/app/src/components/Admin/ExportArchiveData/ArchiveFilesTableMenu.tsx
  40. 1 1
      apps/app/src/components/Admin/ImportData/GrowiArchive/ImportCollectionItem.jsx
  41. 3 3
      apps/app/src/components/Admin/ImportData/ImportDataPageContents.jsx
  42. 2 2
      apps/app/src/components/Admin/LegacySlackIntegration/LegacySlackIntegration.jsx
  43. 2 2
      apps/app/src/components/Admin/LegacySlackIntegration/SlackConfiguration.jsx
  44. 1 1
      apps/app/src/components/Admin/ManageExternalAccount.tsx
  45. 9 10
      apps/app/src/components/Admin/Notification/GlobalNotificationList.jsx
  46. 10 10
      apps/app/src/components/Admin/Notification/ManageGlobalNotification.tsx
  47. 2 2
      apps/app/src/components/Admin/Notification/NotificationDeleteModal.jsx
  48. 2 2
      apps/app/src/components/Admin/Notification/NotificationSetting.jsx
  49. 2 2
      apps/app/src/components/Admin/Security/DeleteAllShareLinksModal.jsx
  50. 1 1
      apps/app/src/components/Admin/Security/GoogleSecuritySettingContents.jsx
  51. 1 1
      apps/app/src/components/Admin/Security/OidcSecuritySettingContents.jsx
  52. 14 1
      apps/app/src/components/Admin/Security/SecuritySetting.jsx
  53. 4 4
      apps/app/src/components/Admin/SlackIntegration/DeleteSlackBotSettingsModal.jsx
  54. 1 1
      apps/app/src/components/Admin/SlackIntegration/SlackAppIntegrationControl.tsx
  55. 2 2
      apps/app/src/components/Admin/UserGroup/UserGroupDeleteModal.tsx
  56. 1 1
      apps/app/src/components/Admin/UserGroup/UserGroupTable.tsx
  57. 3 3
      apps/app/src/components/Admin/UserManagement.tsx
  58. 1 1
      apps/app/src/components/Admin/Users/ExternalAccountTable.tsx
  59. 1 1
      apps/app/src/components/Admin/Users/UserRemoveButton.jsx
  60. 2 2
      apps/app/src/components/AlertSiteUrlUndefined.tsx
  61. 2 2
      apps/app/src/components/Bookmarks/BookmarkFolderItem.tsx
  62. 1 1
      apps/app/src/components/Bookmarks/BookmarkFolderItemControl.tsx
  63. 3 0
      apps/app/src/components/Common/ClosableTextInput.tsx
  64. 37 0
      apps/app/src/components/Common/Dropdown/PageItemControl.spec.tsx
  65. 1 1
      apps/app/src/components/Common/Dropdown/PageItemControl.tsx
  66. 1 1
      apps/app/src/components/Common/PagePathHierarchicalLink/PagePathHierarchicalLink.tsx
  67. 5 0
      apps/app/src/components/Common/PageViewLayout.module.scss
  68. 1 1
      apps/app/src/components/CompleteUserRegistration.tsx
  69. 6 6
      apps/app/src/components/CompleteUserRegistrationForm.tsx
  70. 1 1
      apps/app/src/components/ContentLinkButtons.tsx
  71. 2 2
      apps/app/src/components/DeleteBookmarkFolderModal.tsx
  72. 1 1
      apps/app/src/components/EmptyTrashButton.tsx
  73. 2 2
      apps/app/src/components/EmptyTrashModal.tsx
  74. 3 1
      apps/app/src/components/FontFamily/GlobalFonts.tsx
  75. 17 0
      apps/app/src/components/FontFamily/use-growi-custom-icons.tsx
  76. 2 2
      apps/app/src/components/ForbiddenPage.tsx
  77. 0 20
      apps/app/src/components/Icons/SidebarDockIcon.jsx
  78. 0 25
      apps/app/src/components/Icons/SidebarDrawerIcon.jsx
  79. 2 2
      apps/app/src/components/InAppNotification/InAppNotificationDropdown.tsx
  80. 21 49
      apps/app/src/components/InAppNotification/InAppNotificationElm.tsx
  81. 9 6
      apps/app/src/components/InAppNotification/InAppNotificationList.tsx
  82. 1 3
      apps/app/src/components/InAppNotification/InAppNotificationPage.tsx
  83. 2 11
      apps/app/src/components/InAppNotification/PageNotification/ModelNotification.tsx
  84. 34 25
      apps/app/src/components/InAppNotification/PageNotification/PageModelNotification.tsx
  85. 30 26
      apps/app/src/components/InAppNotification/PageNotification/UserModelNotification.tsx
  86. 19 0
      apps/app/src/components/InAppNotification/PageNotification/index.tsx
  87. 2 19
      apps/app/src/components/InAppNotification/PageNotification/useActionAndMsg.ts
  88. 6 6
      apps/app/src/components/InstallerForm.tsx
  89. 4 4
      apps/app/src/components/InvitedForm.tsx
  90. 1 3
      apps/app/src/components/ItemsTree/ItemsTree.module.scss
  91. 2 2
      apps/app/src/components/ItemsTree/ItemsTree.tsx
  92. 2 0
      apps/app/src/components/Layout/BasicLayout.tsx
  93. 16 14
      apps/app/src/components/LoginForm.tsx
  94. 0 0
      apps/app/src/components/Me/ColorModeSettings.module.scss
  95. 70 0
      apps/app/src/components/Me/ColorModeSettings.tsx
  96. 10 94
      apps/app/src/components/Me/OtherSettings.tsx
  97. 1 1
      apps/app/src/components/Me/PersonalSettings.jsx
  98. 106 0
      apps/app/src/components/Me/QuestionnaireSettings.tsx
  99. 9 0
      apps/app/src/components/Me/UISettings.module.scss
  100. 111 0
      apps/app/src/components/Me/UISettings.tsx

+ 81 - 0
.github/workflows/release-rc-v7.yml

@@ -0,0 +1,81 @@
+name: Release Docker Images for RC (for dev/7.0.x)
+
+on:
+  push:
+    branches:
+      - dev/7.0.x
+
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+
+jobs:
+
+  determine-tags:
+    runs-on: ubuntu-latest
+
+    outputs:
+      TAGS: ${{ steps.meta.outputs.tags }}
+      TAGS_GHCR: ${{ steps.meta-ghcr.outputs.tags }}
+
+    steps:
+    - uses: actions/checkout@v3
+
+    - name: Retrieve information from package.json
+      uses: myrotvorets/info-from-package-json-action@1.2.0
+      id: package-json
+
+    - name: Docker meta for docker.io
+      uses: docker/metadata-action@v4
+      id: meta
+      with:
+        images: docker.io/weseek/growi
+        sep-tags: ','
+        tags: |
+          type=raw,value=${{ steps.package-json.outputs.packageVersion }}.{{sha}}
+
+    - name: Docker meta for ghcr.io
+      uses: docker/metadata-action@v4
+      id: meta-ghcr
+      with:
+        images: ghcr.io/weseek/growi
+        sep-tags: ','
+        tags: |
+          type=raw,value=${{ steps.package-json.outputs.packageVersion }}.{{sha}}
+
+
+  build-image-rc:
+    uses: weseek/growi/.github/workflows/reusable-app-build-image.yml@master
+    with:
+      image-name: weseek/growi
+      tag-temporary: latest-rc
+    secrets:
+      AWS_ROLE_TO_ASSUME_FOR_OIDC: ${{ secrets.AWS_ROLE_TO_ASSUME_FOR_OIDC }}
+
+
+  publish-image-rc:
+    needs: [determine-tags, build-image-rc]
+
+    uses: weseek/growi/.github/workflows/reusable-app-create-manifests.yml@master
+    with:
+      tags: ${{ needs.determine-tags.outputs.TAGS }}
+      registry: docker.io
+      image-name: weseek/growi
+      tag-temporary: latest-rc
+    secrets:
+      DOCKER_REGISTRY_PASSWORD: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
+
+  publish-image-rc-ghcr:
+    needs: [determine-tags, build-image-rc]
+
+    uses: weseek/growi/.github/workflows/reusable-app-create-manifests.yml@master
+    with:
+      tags: ${{ needs.determine-tags.outputs.TAGS_GHCR }}
+      registry: ghcr.io
+      image-name: weseek/growi
+      tag-temporary: latest-rc
+    secrets:
+      DOCKER_REGISTRY_PASSWORD: ${{ secrets.DOCKER_REGISTRY_ON_GITHUB_PASSWORD }}
+

+ 3 - 3
.vscode/settings.json

@@ -12,9 +12,9 @@
   "scss.validate": false,
 
   "editor.codeActionsOnSave": {
-    "source.fixAll.eslint": true,
-    "source.fixAll.markdownlint": true,
-    "source.fixAll.stylelint": true
+    "source.fixAll.eslint": "explicit",
+    "source.fixAll.markdownlint": "explicit",
+    "source.fixAll.stylelint": "explicit"
   },
 
   "githubPullRequests.ignoredPullRequestBranches": [

+ 53 - 1
CHANGELOG.md

@@ -1,9 +1,61 @@
 # Changelog
 
-## [Unreleased](https://github.com/weseek/growi/compare/v6.2.3...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v6.3.0...HEAD)
 
 *Please do not manually update this file. We've automated the process.*
 
+## [v6.3.0](https://github.com/weseek/growi/compare/v6.2.5...v6.3.0) - 2023-12-14
+
+### BREAKING CHANGES
+
+* support: Remove obsolete route for attachment on MongoDB GridFS (#8239) @yuki-takei
+
+### 💎 Features
+
+* feat: LDAP/Keycloak group sync (#7857) @arafubeatbox
+
+### 🚀 Improvement
+
+* imprv: Refactor DrawioViewer re-rendering by the resizing trigger (#8314) @yuki-takei
+* imprv: Apply content headers for attachment response (#8245) @yuki-takei
+
+### 🐛 Bug Fixes
+
+* fix: SAML callback action throws the field is undefined error when the ACL Rule string is only white space (#8322) @yuki-takei
+* fix: Remove groups not related to the user from the user groups that are specified automatically when creating child pages (#8266) @arafubeatbox
+* fix: Certify shared page attachment middleware (#8255) @yuki-takei
+
+### 🧰 Maintenance
+
+* support: Add test for delete-completely-user-home-by-system.ts (#8323) @jam411
+* ci(deps-dev): bump vite from 4.5.0 to 4.5.1 (#8302) @dependabot
+* support: TypeScriptize attachment codes (#8243) @yuki-takei
+* support: Remove obsolete route for attachment on MongoDB GridFS (#8239) @yuki-takei
+
+## [v6.2.5](https://github.com/weseek/growi/compare/v6.2.4...v6.2.5) - 2023-12-14
+
+### 🐛 Bug Fixes
+
+* fix: Update deleteCompletelyUserHomeBySystem for v4 process (#8289) @jam411
+
+## [v6.2.4](https://github.com/weseek/growi/compare/v6.2.3...v6.2.4) - 2023-11-29
+
+### 💎 Features
+
+* feat: Show create date in Attachment Data list (#8229) @sakazuki
+
+### 🚀 Improvement
+
+* imprv: Add Marp preset template for ja_JP and zh_CN (#8179) @AikaHiyama
+* imprv: Allow deletion of user homepage when the user is deleted (#8224) @jam411
+
+### 🐛 Bug Fixes
+* fix: Certify shared page attachment middleware (6.2.x) (#8256) @yuki-takei
+
+### 🧰 Maintenance
+
+* support: Refactor deleteCompletelyUserHomeBySystem (#8262) @jam411
+
 ## [v6.2.3](https://github.com/weseek/growi/compare/v6.2.2...v6.2.3) - 2023-11-13
 
 ### 🚀 Improvement

+ 1 - 0
apps/app/.eslintrc.js

@@ -27,6 +27,7 @@ module.exports = {
       },
     ]],
     '@typescript-eslint/no-var-requires': 'off',
+    '@typescript-eslint/consistent-type-imports': 'warn',
 
     // set 'warn' temporarily -- 2021.08.02 Yuki Takei
     '@typescript-eslint/no-use-before-define': ['warn'],

+ 2 - 2
apps/app/_obsolete/src/components/Navbar/GrowiNavbar.tsx

@@ -47,7 +47,7 @@ const NavbarRight = memo((): JSX.Element => {
                 data-testid="newPageBtn"
                 onClick={() => openCreateModal(currentPagePath || '')}
               >
-                <i className="icon-pencil me-2"></i>
+                <span className="material-symbols-outlined">edit</span>
                 <span className="d-none d-lg-block">{ t('commons:New') }</span>
               </button>
             </li>
@@ -85,7 +85,7 @@ const Confidential: FC<ConfidentialProps> = memo((props: ConfidentialProps): JSX
 
   return (
     <li className="nav-item confidential text-light">
-      <i id="confidentialTooltip" className="icon-info d-md-none" />
+      <i id="confidentialTooltip"></i><span className="material-symbols-outlined d-md-none">info</span>
       <span className="d-none d-md-inline">
         {confidential}
       </span>

+ 1 - 1
apps/app/_obsolete/src/components/PageEditor/CodeMirrorEditor.jsx

@@ -727,7 +727,7 @@ class CodeMirrorEditor extends AbstractEditor {
   renderCheatsheetModalButton() {
     return (
       <button type="button" className="btn-link gfm-cheatsheet-modal-link small" onClick={() => { this.markdownHelpButtonClickedHandler() }}>
-        <i className="icon-question" /> Markdown
+        <span className="material-symbols-outlined">help</span> Markdown
       </button>
     );
   }

+ 4 - 4
apps/app/_obsolete/src/components/PageEditor/ConflictDiffModal.tsx

@@ -156,7 +156,7 @@ const ConflictDiffModalCore = (props: ConflictDiffModalCoreProps): JSX.Element =
     >
       {/* <ModalHeader tag="h4" toggle={onClose} className="bg-primary text-light align-items-center py-3" close={resizeAndCloseButtons}> */}
       <ModalHeader tag="h4" toggle={onClose} className="bg-primary text-light align-items-center py-3">
-        <i className="icon-fw icon-exclamation" />{t('modal_resolve_conflict.resolve_conflict')}
+        <span className="material-symbols-outlined">error</span>{t('modal_resolve_conflict.resolve_conflict')}
       </ModalHeader>
       <ModalBody className="mx-4 my-1">
         { isOpen
@@ -212,7 +212,7 @@ const ConflictDiffModalCore = (props: ConflictDiffModalCoreProps): JSX.Element =
                     setResolvedRevision(request.revisionBody);
                   }}
                 >
-                  <i className="icon-fw icon-arrow-down-circle"></i>
+                  <span className="material-symbols-outlined">arrow_circle_down</span>
                   {t('modal_resolve_conflict.select_revision', { revision: 'mine' })}
                 </button>
               </div>
@@ -227,7 +227,7 @@ const ConflictDiffModalCore = (props: ConflictDiffModalCoreProps): JSX.Element =
                     setResolvedRevision(origin.revisionBody);
                   }}
                 >
-                  <i className="icon-fw icon-arrow-down-circle"></i>
+                  <span className="material-symbols-outlined">arrow_circle_down</span>
                   {t('modal_resolve_conflict.select_revision', { revision: 'origin' })}
                 </button>
               </div>
@@ -242,7 +242,7 @@ const ConflictDiffModalCore = (props: ConflictDiffModalCoreProps): JSX.Element =
                     setResolvedRevision(latest.revisionBody);
                   }}
                 >
-                  <i className="icon-fw icon-arrow-down-circle"></i>
+                  <span className="material-symbols-outlined">arrow_circle_down</span>
                   {t('modal_resolve_conflict.select_revision', { revision: 'theirs' })}
                 </button>
               </div>

+ 15 - 13
apps/app/src/components/PageEditor/MarkdownTableInterceptor.js → apps/app/_obsolete/src/components/PageEditor/MarkdownTableInterceptor.js

@@ -2,7 +2,10 @@ import { BasicInterceptor } from '@growi/core/dist/utils';
 
 import MarkdownTable from '~/client/models/MarkdownTable';
 
-import mtu from './MarkdownTableUtil';
+import {
+  getStrFromBot, addRowToMarkdownTable, getStrToEot, isEndOfLine, mergeMarkdownTable, replaceFocusedMarkdownTableWithEditor,
+  isInTable, emptyLineOfTableRE,
+} from '../../../../src/components/PageEditor/markdown-table-util-for-editor';
 
 /**
  * Interceptor for markdown table
@@ -27,24 +30,24 @@ export default class MarkdownTableInterceptor extends BasicInterceptor {
 
   addRow(cm) {
     // get lines all of table from current position to beginning of table
-    const strFromBot = mtu.getStrFromBot(cm);
+    const strFromBot = getStrFromBot(cm);
     let table = MarkdownTable.fromMarkdownString(strFromBot);
 
-    mtu.addRowToMarkdownTable(table);
+    addRowToMarkdownTable(table);
 
-    const strToEot = mtu.getStrToEot(cm);
+    const strToEot = getStrToEot(cm);
     const tableBottom = MarkdownTable.fromMarkdownString(strToEot);
     if (tableBottom.table.length > 0) {
-      table = mtu.mergeMarkdownTable([table, tableBottom]);
+      table = mergeMarkdownTable([table, tableBottom]);
     }
 
-    mtu.replaceMarkdownTableWithReformed(cm, table);
+    replaceFocusedMarkdownTableWithEditor(cm, table);
   }
 
   reformTable(cm) {
-    const tableStr = mtu.getStrFromBot(cm) + mtu.getStrToEot(cm);
+    const tableStr = getStrFromBot(cm) + getStrToEot(cm);
     const table = MarkdownTable.fromMarkdownString(tableStr);
-    mtu.replaceMarkdownTableWithReformed(cm, table);
+    replaceFocusedMarkdownTableWithEditor(cm, table);
   }
 
   removeRow(editor) {
@@ -67,16 +70,15 @@ export default class MarkdownTableInterceptor extends BasicInterceptor {
 
     const cm = editor.getCodeMirror();
 
-    const isInTable = mtu.isInTable(cm);
-    const isLastRow = mtu.getStrToEot(cm) === editor.getStrToEol();
+    const isLastRow = getStrToEot(cm) === editor.getStrToEol();
 
-    if (isInTable) {
+    if (isInTable(cm)) {
       // at EOL in the table
-      if (mtu.isEndOfLine(cm)) {
+      if (isEndOfLine(cm)) {
         this.addRow(cm);
       }
       // last empty row
-      else if (isLastRow && mtu.emptyLineOfTableRE.test(editor.getStrFromBol() + editor.getStrToEol())) {
+      else if (isLastRow && emptyLineOfTableRE.test(editor.getStrFromBol() + editor.getStrToEol())) {
         this.removeRow(editor);
       }
       else {

+ 9 - 9
apps/app/_obsolete/src/components/PageEditorByHackmd.tsx

@@ -347,7 +347,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
     if (hackmdUri == null) {
       content = (
         <div>
-          <p className="text-center hackmd-status-label"><i className="fa fa-file-text"></i> { t('hackmd.not_set_up')}</p>
+          <p className="text-center hackmd-status-label"><span className="material-symbols-outlined">description</span> { t('hackmd.not_set_up')}</p>
           {/* eslint-disable-next-line react/no-danger */}
           <p dangerouslySetInnerHTML={{ __html: t('hackmd.need_to_associate_with_growi_to_use_hackmd_refer_to_this') }} />
         </div>
@@ -361,7 +361,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
       content = (
         <div className="text-center">
           <p className="hackmd-status-label">
-            <i className="fa fa-file-text me-2" />
+            <span className="material-symbols-outlined">description</span>
             { t('hackmd.used_for_not_found') }
           </p>
           {/* eslint-disable-next-line react/no-danger */}
@@ -377,12 +377,12 @@ export const PageEditorByHackmd = (): JSX.Element => {
 
       content = (
         <div>
-          <p className="text-center hackmd-status-label"><i className="fa fa-file-text"></i> HackMD is READY!</p>
+          <p className="text-center hackmd-status-label"><span className="material-symbols-outlined">description</span> HackMD is READY!</p>
           <p className="text-center"><strong>{t('hackmd.unsaved_draft')}</strong></p>
 
           { isHackmdDocumentOutdated && (
             <div className="card border-warning">
-              <div className="card-header bg-warning text-dark"><i className="icon-fw icon-info"></i> {t('hackmd.draft_outdated')}</div>
+              <div className="card-header bg-warning text-dark"><span className="material-symbols-outlined">info</span> {t('hackmd.draft_outdated')}</div>
               <div className="card-body text-center">
                 {t('hackmd.based_on_revision')}&nbsp;
                 { pageData != null && (
@@ -412,7 +412,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
                 disabled={isInitializing}
                 onClick={resumeToEdit}
               >
-                <span className="btn-label"><i className="icon-fw icon-control-end"></i></span>
+                <span className="btn-label"></span><span className="material-symbols-outlined">skip_next</span>
                 <span className="btn-text">{t('hackmd.resume_to_edit')}</span>
               </button>
             </div>
@@ -424,7 +424,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
               type="button"
               onClick={discardChanges}
             >
-              <span className="btn-label"><i className="icon-fw icon-control-start"></i></span>
+              <span className="btn-label"></span><span className="material-symbols-outlined">play_arrow</span>
               <span className="btn-text">{t('hackmd.discard_changes')}</span>
             </button>
           </div>
@@ -440,7 +440,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
 
       content = (
         <div>
-          <p className="text-muted text-center hackmd-status-label"><i className="fa fa-file-text"></i> HackMD is READY!</p>
+          <p className="text-muted text-center hackmd-status-label"><span className="material-symbols-outlined">description</span> HackMD is READY!</p>
           <div className="text-center hackmd-start-button-container mb-3">
             <button
               className="btn btn-info btn-lg waves-effect waves-light"
@@ -448,7 +448,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
               disabled={isRevisionOutdated || isInitializing}
               onClick={startToEdit}
             >
-              <span className="btn-label"><i className="icon-fw icon-paper-plane"></i></span>
+              <span className="btn-label"></span><span className="material-symbols-outlined">send</span>
               {t('hackmd.start_to_edit')}
             </button>
           </div>
@@ -504,7 +504,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
       { hasError && (
         <div className="hackmd-error position-absolute d-flex flex-column justify-content-center align-items-center">
           <div className="bg-box p-5 text-center">
-            <h2 className="text-warning"><i className="icon-fw icon-exclamation"></i> {t('hackmd.integration_failed')}</h2>
+            <h2 className="text-warning"><span className="material-symbols-outlined">error</span> {t('hackmd.integration_failed')}</h2>
             <h4>{errorMessage}</h4>
             <p className="card custom-card text-danger">
               {errorReason}

+ 2 - 1
apps/app/docker/README.md

@@ -11,7 +11,8 @@ Supported tags and respective Dockerfile links
 ------------------------------------------------
 
 * [`7.0.0`, `7.0`, `7`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v7.0.0/apps/app/docker/Dockerfile)
-* [`6.2.3`, `6.2`, `6` (Dockerfile)](https://github.com/weseek/growi/blob/v6.2.3/apps/app/docker/Dockerfile)
+* [`6.3.0`, `6.3`, `6` (Dockerfile)](https://github.com/weseek/growi/blob/v6.3.0/apps/app/docker/Dockerfile)
+* [`6.2.4`, `6.2` (Dockerfile)](https://github.com/weseek/growi/blob/v6.2.4/apps/app/docker/Dockerfile)
 * [`6.1.15`, `6.1` (Dockerfile)](https://github.com/weseek/growi/blob/v6.1.15/apps/app/docker/Dockerfile)
 
 

+ 11 - 3
apps/app/package.json

@@ -35,11 +35,12 @@
     "prelint:swagger2openapi": "yarn openapi:v3",
     "test": "run-p test:*",
     "test:jest": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=4096\" jest --logHeapUsage",
-    "test:vitest": "run-p vitest:run vitest:run:integ",
+    "test:vitest": "run-p vitest:run vitest:run:integ vitest:run:components",
     "jest:run": "cross-env NODE_ENV=test jest --passWithNoTests -- ",
     "reg:run": "reg-suit run",
     "vitest:run": "vitest run config src --coverage",
     "vitest:run:integ": "vitest run -c vitest.config.integ.ts src --coverage",
+    "vitest:run:components": "vitest run -c vitest.config.components.ts src --coverage",
     "previtest:run:integ": "vitest run -c test-with-vite/download-mongo-binary/vitest.config.ts test-with-vite/download-mongo-binary",
     "//// misc": "",
     "console": "yarn cross-env NODE_ENV=development yarn ts-node --experimental-repl-await src/server/console.js",
@@ -68,6 +69,7 @@
     "@elastic/elasticsearch8": "npm:@elastic/elasticsearch@^8.7.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
+    "@growi/custom-icons": "link:../../packages/custom-icons",
     "@growi/core": "link:../../packages/core",
     "@growi/pluginkit": "link:../../packages/pluginkit",
     "@growi/preset-templates": "link:../../packages/preset-templates",
@@ -174,6 +176,7 @@
     "react-markdown": "^8.0.7",
     "react-multiline-clamp": "^2.0.0",
     "react-scroll": "^1.8.7",
+    "react-stickynode": "^4.1.0",
     "react-syntax-highlighter": "^15.5.0",
     "react-toastify": "^9.1.3",
     "react-use-ripple": "^1.5.2",
@@ -203,7 +206,7 @@
     "uglifycss": "^0.0.29",
     "universal-bunyan": "^0.9.2",
     "unstated": "^2.1.1",
-    "unzipper": "^0.10.5",
+    "unzip-stream": "^0.3.1",
     "url-join": "^4.0.0",
     "usehooks-ts": "^2.6.0",
     "validator": "^13.7.0",
@@ -223,17 +226,22 @@
     "@next/bundle-analyzer": "^13.2.3",
     "@swc-node/jest": "^1.6.2",
     "@swc/jest": "^0.2.24",
+    "@testing-library/react": "^14.1.2",
+    "@testing-library/user-event": "^14.5.2",
     "@types/express": "^4.17.11",
     "@types/jest": "^29.5.2",
     "@types/react-scroll": "^1.8.4",
     "@types/throttle-debounce": "^5.0.1",
     "@types/url-join": "^4.0.2",
+    "@types/unzip-stream": "^0.3.4",
+    "@vitejs/plugin-react": "^4.2.1",
     "@vitest/coverage-v8": "^0.34.6",
     "autoprefixer": "^9.0.0",
     "babel-loader": "^8.2.5",
     "bootstrap": "^5.3.1",
     "connect-browser-sync": "^2.1.0",
     "diff2html": "^3.4.35",
+    "downshift": "^8.2.3",
     "eazy-logger": "^3.1.0",
     "emoji-mart": "npm:panta82-emoji-mart@^3.0.1",
     "eslint-plugin-cypress": "^2.12.1",
@@ -242,6 +250,7 @@
     "font-awesome": "^4.7.0",
     "fslightbox-react": "^1.7.6",
     "handsontable": "=6.2.2",
+    "happy-dom": "^13.2.0",
     "i18next-hmr": "^1.11.0",
     "jest": "^29.5.0",
     "jest-date-mock": "^1.0.8",
@@ -258,7 +267,6 @@
     "react-copy-to-clipboard": "^5.0.1",
     "react-dropzone": "^11.2.4",
     "react-hotkeys": "^2.0.0",
-    "react-stickynode": "^4.1.0",
     "rehype-rewrite": "^3.0.6",
     "replacestream": "^4.0.3",
     "sass": "^1.53.0",

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

@@ -48,8 +48,9 @@
     "anyone": "Anyone",
     "user_homepage_deletion": {
       "user_homepage_deletion": "User homepage deletion",
-      "enable_user_homepage_deletion": "Complete deletion of user homepage, when user deletion",
-      "desc": "When deleting a user, the user homepage and its sub pages are also completely deleted."
+      "enable_user_homepage_deletion": "Enable user homepage deletion",
+      "enable_force_delete_user_homepage_on_user_deletion": "When you delete a user, the user's homepage and all its sub pages will be completely deleted",
+      "desc": "You will be able to delete a deleted user's homepage."
     },
     "session": "Session",
     "max_age": "Max age (msec)",
@@ -1106,7 +1107,7 @@
       "group_sync_client_secret_detail": "Id of the secret used to authenticate to request to Keycloak admin API",
       "updated_group_sync_settings": "Updated Keycloak group sync settings",
       "preserve_deleted_keycloak_groups": "Preserve Deleted Keycloak Groups",
-      "auth_not_set": "Enable and configure OIDC or SAML with Keycloak in security settings before sync"
+      "auth_not_set": "Enable OIDC or SAML host that includes 'Host' and 'Group Realm' of group sync settings"
     },
     "auto_generate_user_on_sync": "Auto Generate User on Sync",
     "description_mapper_detail": "Attribute to map as group description. Description can be edited after sync. However, when a mapper is set, the edited value can possibly be overwritten by the next sync."

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

@@ -42,6 +42,12 @@
     }
   },
 
+  "search_method_menu_item": {
+    "search_in_all": "Search in all",
+    "only_children_of_this_tree": "Only children of this tree",
+    "exact_mutch": "Exact match"
+  },
+
   "share_links": {
     "Share Link": "Share Link",
     "Page Path": "Page Path",
@@ -71,7 +77,7 @@
   "create_page_dropdown": {
     "new_page": "Create New Page",
     "todays": {
-      "desc": "Create today's ...",
+      "desc": "Create today's memo",
       "memo": "memo"
     },
     "template": {

+ 19 - 0
apps/app/public/static/locales/en_US/translation.json

@@ -144,6 +144,7 @@
   "wide_view": "Wide View",
   "Recent Changes": "Recent Changes",
   "Page Tree": "Page Tree",
+  "In-App Notification": "Notifications",
   "original_path":"Original path",
   "new_path":"New path",
   "duplicated_path":"Duplicated path",
@@ -249,6 +250,21 @@
       "page_create": "Subscribe to the page when you create it."
     }
   },
+  "ui_settings": {
+    "ui_settings": "UI Settings",
+    "side_bar_mode": {
+      "settings": "Sidebar mode settings",
+      "side_bar_mode_setting": "Set the sidebar mode",
+      "description": "You can set whether or not the sidebar will always be open when the screen width is large. If the screen width is small, the sidebar will always be closed."
+    }
+  },
+  "color_mode_settings": {
+    "light": "Light",
+    "dark": "Dark",
+    "system": "System",
+    "settings": "Color mode settings",
+    "description": "Select whether to display in light mode, dark mode, or a system-specific display.<br>Only supported themes can be switched."
+  },
   "editor_settings": {
     "editor_settings": "Editor Settings"
   },
@@ -803,5 +819,8 @@
   },
   "rich_attachment": {
     "attachment_not_be_found": "The attachment could not be found"
+  },
+  "page_select_modal": {
+    "select_page_location": "Select page location"
   }
 }

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

@@ -57,8 +57,9 @@
     "anyone": "誰でも可能",
     "user_homepage_deletion": {
       "user_homepage_deletion": "ユーザーホームページの削除",
-      "enable_user_homepage_deletion": "ユーザー削除時にユーザーホームページを完全削除する",
-      "desc": "ユーザーを削除する際に、ユーザーホームページとその配下のページも完全削除されます。"
+      "enable_user_homepage_deletion": "ユーザーホームページの削除を有効化",
+      "enable_force_delete_user_homepage_on_user_deletion": "ユーザーを削除したとき、ユーザーホームページとその配下のページを完全削除する",
+      "desc": "削除済みユーザーのユーザーホームページを削除できるようになります。"
     },
     "session": "セッション",
     "max_age": "有効期間 (ミリ秒)",
@@ -1116,7 +1117,7 @@
       "group_sync_client_secret_detail": "Keycloak admin API にリクエストするための認証に使う client の secret",
       "updated_group_sync_settings": "Keycloak グループ同期設定を更新しました",
       "preserve_deleted_keycloak_groups": "Keycloak から削除されたグループを GROWI に残す",
-      "auth_not_set": "同期実行前にセキュリティ設定で Keycloak を使った OIDC または SAML 認証を有効にし、設定してください"
+      "auth_not_set": "グループ同期設定の Host と Group Realm が発行ホストに含まれる OIDC または SAML 認証をセキュリティ設定で有効にしてください"
     },
     "auto_generate_user_on_sync": "作成されていない GROWI アカウントを自動生成する",
     "description_mapper_detail": "グループの「説明」として読み込む属性。「説明」は同期後に編集可能です。ただし、mapper が設定されている場合、編集内容は再同期によって上書きされます。"

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

@@ -44,6 +44,12 @@
     }
   },
 
+  "search_method_menu_item": {
+    "search_in_all": "全てのページ",
+    "only_children_of_this_tree": "この階層下の子ページのみ",
+    "exact_mutch": "キーワードに完全一致した文字を含むページのみ"
+  },
+
   "share_links": {
     "Share Link": "共有用リンク",
     "Page Path": "ページパス",
@@ -73,7 +79,7 @@
   "create_page_dropdown": {
     "new_page": "新規ページ作成",
     "todays": {
-      "desc": "今日の◯◯を作成",
+      "desc": "今日のメモを作成",
       "memo": "メモ"
     },
     "template": {

+ 19 - 0
apps/app/public/static/locales/ja_JP/translation.json

@@ -145,6 +145,7 @@
   "wide_view": "ワイドビュー",
   "Recent Changes": "最新の変更",
   "Page Tree": "ページツリー",
+  "In-App Notification": "通知",
   "original_path":"元のパス",
   "new_path":"新しいパス",
   "duplicated_path":"重複したパス",
@@ -250,6 +251,21 @@
       "page_create": "ページを作成した時にそのページをサブスクライブします。"
     }
   },
+  "ui_settings": {
+    "ui_settings": "UI設定",
+    "side_bar_mode": {
+      "settings": "サイドバーモードの設定",
+      "side_bar_mode_setting": "サイドバーのモードを設定する",
+      "description": "画面幅が大きい場合に、サイドバーを常時開いた状態にするかどうかを設定できます。画面幅が小さい場合はサイドバーは常に閉じた状態となります。"
+    }
+  },
+  "color_mode_settings": {
+    "light": "ライト",
+    "dark": "ダーク",
+    "system": "システム",
+    "settings": "カラーモードの設定",
+    "description": "ライトモードかダークモード、もしくはシステム合わせた表示をするか選択します。<br>対応したテーマのみ切り替えることができます。"
+  },
   "editor_settings": {
     "editor_settings": "エディター設定",
     "common_settings": {
@@ -836,5 +852,8 @@
   },
   "rich_attachment": {
     "attachment_not_be_found": "アタッチメントが見つかりません"
+  },
+  "page_select_modal": {
+    "select_page_location": "ページの場所を選択"
   }
 }

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

@@ -56,9 +56,10 @@
 		"admin_and_author": "管理员|作者",
 		"anyone": "任何人",
     "user_homepage_deletion": {
-      "user_homepage_deletion": "删除用户页面",
-      "enable_user_homepage_deletion": "用户删除时,完全删除用户主页",
-      "desc": "删除用户时,用户主页及其下属页面也会被完全删除。"
+      "user_homepage_deletion": "删除用户主页",
+      "enable_user_homepage_deletion": "启用用户主页删除功能",
+      "enable_force_delete_user_homepage_on_user_deletion": "删除用户时,该用户的主页及其所有子页面将被完全删除",
+      "desc": "您可以删除已删除用户的主页。"
     },
     "session": "会议",
     "max_age": "有效期间  (msec)",
@@ -1115,7 +1116,7 @@
       "group_sync_client_secret_detail": "Id of the secret used to authenticate to request to Keycloak admin API",
       "updated_group_sync_settings": "Updated Keycloak group sync settings",
       "preserve_deleted_keycloak_groups": "Preserve Deleted Keycloak Groups",
-      "auth_not_set": "Enable and configure OIDC or SAML with Keycloak in security settings before sync"
+      "auth_not_set": "Enable OIDC or SAML host that includes 'Host' and 'Group Realm' of group sync settings"
     },
     "auto_generate_user_on_sync": "Auto Generate User on Sync",
     "description_mapper_detail": "Attribute to map as group description. Description can be edited after sync. However, when a mapper is set, the edited value can possibly be overwritten by the next sync."

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

@@ -45,6 +45,12 @@
 		}
   },
 
+  "search_method_menu_item": {
+    "search_in_all": "所有页面",
+    "only_children_of_this_tree": "当前分支以下内容",
+    "exact_mutch": "完全匹配"
+  },
+
   "share_links": {
     "Share Link": "Share Link",
     "Page Path": "Page Path",
@@ -74,7 +80,7 @@
   "create_page_dropdown": {
     "new_page": "新页面",
     "todays": {
-      "desc": "Create today's ...",
+      "desc": "Create today's memo",
       "memo": "memo"
     },
     "template": {

+ 19 - 0
apps/app/public/static/locales/zh_CN/translation.json

@@ -150,6 +150,7 @@
   "wide_view": "视野开阔",
   "Recent Changes": "最新修改",
   "Page Tree": "页面树",
+  "In-App Notification": "通知",
   "original_path":"Original path",
   "new_path":"New path",
   "duplicated_path":"Duplicated path",
@@ -240,6 +241,21 @@
       "page_create": "创建页面时订阅页面。"
     }
   },
+  "ui_settings": {
+    "ui_settings": "用户界面设置",
+    "side_bar_mode": {
+      "settings": "侧边栏模式设置",
+      "side_bar_mode_setting": "设置侧边栏模式",
+      "description": "您可以设置当屏幕宽度较大时,侧边栏是否始终打开。 如果屏幕宽度较小,侧边栏将始终关闭。"
+    }
+  },
+  "color_mode_settings": {
+    "light": "灯光",
+    "dark": "暗处",
+    "system": "系统",
+    "settings": "色彩模式设置",
+    "description": "选择是以浅色模式、深色模式还是系统特定的显示方式显示。<br>只能切换支持的主题。"
+  },
   "editor_settings": {
     "editor_settings": "编辑器设置"
   },
@@ -806,5 +822,8 @@
   },
   "rich_attachment": {
     "attachment_not_be_found": "没有找到附件"
+  },
+  "page_select_modal": {
+    "select_page_location": "选择页面位置"
   }
 }

+ 10 - 0
apps/app/src/client/services/AdminGeneralSecurityContainer.js

@@ -39,6 +39,7 @@ export default class AdminGeneralSecurityContainer extends Container {
       isShowRestrictedByOwner: false,
       isShowRestrictedByGroup: false,
       isUsersHomepageDeletionEnabled: false,
+      isForceDeleteUserHomepageOnUserDeletion: false,
       isLocalEnabled: false,
       isLdapEnabled: false,
       isSamlEnabled: false,
@@ -75,6 +76,7 @@ export default class AdminGeneralSecurityContainer extends Container {
       isShowRestrictedByOwner: !generalSetting.hideRestrictedByOwner,
       isShowRestrictedByGroup: !generalSetting.hideRestrictedByGroup,
       isUsersHomepageDeletionEnabled: generalSetting.isUsersHomepageDeletionEnabled,
+      isForceDeleteUserHomepageOnUserDeletion: generalSetting.isForceDeleteUserHomepageOnUserDeletion,
       sessionMaxAge: generalSetting.sessionMaxAge,
       wikiMode: generalSetting.wikiMode,
       disableLinkSharing: shareLinkSetting.disableLinkSharing,
@@ -202,6 +204,13 @@ export default class AdminGeneralSecurityContainer extends Container {
     this.setState({ isUsersHomepageDeletionEnabled: !this.state.isUsersHomepageDeletionEnabled });
   }
 
+  /**
+   * Switch isForceDeleteUserHomepageOnUserDeletion
+   */
+  switchIsForceDeleteUserHomepageOnUserDeletion() {
+    this.setState({ isForceDeleteUserHomepageOnUserDeletion: !this.state.isForceDeleteUserHomepageOnUserDeletion });
+  }
+
   /**
    * Update restrictGuestMode
    * @memberOf AdminGeneralSecuritySContainer
@@ -219,6 +228,7 @@ export default class AdminGeneralSecurityContainer extends Container {
       hideRestrictedByGroup: !this.state.isShowRestrictedByGroup,
       hideRestrictedByOwner: !this.state.isShowRestrictedByOwner,
       isUsersHomepageDeletionEnabled: this.state.isUsersHomepageDeletionEnabled,
+      isForceDeleteUserHomepageOnUserDeletion: this.state.isForceDeleteUserHomepageOnUserDeletion,
     };
 
     requestParams = await removeNullPropertyFromObject(requestParams);

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

@@ -5,7 +5,7 @@ import EventEmitter from 'events';
 import type { DrawioEditByViewerProps } from '@growi/remark-drawio';
 
 import { useSaveOrUpdate } from '~/client/services/page-operation';
-import mdu from '~/components/PageEditor/MarkdownDrawioUtil';
+import { replaceDrawioInMarkdown } from '~/components/Page/markdown-drawio-util-for-view';
 import type { OptionsToSave } from '~/interfaces/page-operation';
 import { useShareLinkId } from '~/stores/context';
 import { useDrawioModal } from '~/stores/modal';
@@ -41,7 +41,7 @@ export const useDrawioModalLauncherForView = (opts?: {
     }
 
     const currentMarkdown = currentPage.revision.body;
-    const newMarkdown = mdu.replaceDrawioInMarkdown(drawioMxFile, currentMarkdown, bol, eol);
+    const newMarkdown = replaceDrawioInMarkdown(drawioMxFile, currentMarkdown, bol, eol);
 
     const grantUserGroupIds = currentPage.grantedGroups.map((g) => {
       return {

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

@@ -4,7 +4,7 @@ import EventEmitter from 'events';
 
 import MarkdownTable from '~/client/models/MarkdownTable';
 import { useSaveOrUpdate } from '~/client/services/page-operation';
-import mtu from '~/components/PageEditor/MarkdownTableUtil';
+import { getMarkdownTableFromLine, replaceMarkdownTableInMarkdown } from '~/components/Page/markdown-table-util-for-view';
 import type { OptionsToSave } from '~/interfaces/page-operation';
 import { useShareLinkId } from '~/stores/context';
 import { useHandsontableModal } from '~/stores/modal';
@@ -40,7 +40,7 @@ export const useHandsontableModalLauncherForView = (opts?: {
     }
 
     const currentMarkdown = currentPage.revision.body;
-    const newMarkdown = mtu.replaceMarkdownTableInMarkdown(table, currentMarkdown, bol, eol);
+    const newMarkdown = replaceMarkdownTableInMarkdown(table, currentMarkdown, bol, eol);
 
     const grantUserGroupIds = currentPage.grantedGroups.map((g) => {
       return {
@@ -82,8 +82,8 @@ export const useHandsontableModalLauncherForView = (opts?: {
 
     const handler = (bol: number, eol: number) => {
       const markdown = currentPage.revision.body;
-      const currentMarkdownTable = mtu.getMarkdownTableFromLine(markdown, bol, eol);
-      openHandsontableModal(currentMarkdownTable, undefined, false, table => saveByHandsontableModal(table, bol, eol));
+      const currentMarkdownTable = getMarkdownTableFromLine(markdown, bol, eol);
+      openHandsontableModal(currentMarkdownTable, false, table => saveByHandsontableModal(table, bol, eol));
     };
     globalEmitter.on('launchHandsonTableModal', handler);
 

+ 15 - 9
apps/app/src/client/services/use-on-template-button-clicked.ts

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

+ 6 - 0
apps/app/src/client/services/user-ui-settings.ts

@@ -25,3 +25,9 @@ export const scheduleToPut = (settings: Partial<IUserUISettings>): void => {
 
   _putUserUISettingsInBulkDebounced();
 };
+
+export const updateUserUISettings = async(settings: Partial<IUserUISettings>): Promise<AxiosResponse<IUserUISettings>> => {
+  const result = await apiv3Put<IUserUISettings>('/user-ui-settings', { settings });
+
+  return result;
+};

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

@@ -32,7 +32,7 @@ export const ConfirmModal: FC<ConfirmModalProps> = (props: ConfirmModalProps) =>
   return (
     <Modal isOpen={props.isModalOpen} toggle={onCancel}>
       <ModalHeader tag="h4" toggle={onCancel} className="bg-danger">
-        <i className="icon-fw icon-question" />
+        <span className="material-symbols-outlined">help</span>
         {t('Warning')}
       </ModalHeader>
       <ModalBody>
@@ -44,7 +44,7 @@ export const ConfirmModal: FC<ConfirmModalProps> = (props: ConfirmModalProps) =>
               <br />
               <span className="text-warning">
                 <>
-                  <i className="icon-exclamation icon-fw"></i>
+                  <span className="material-symbols-outlined">error</span>
                   {props.supplymentaryMessage}
                 </>
               </span>

+ 3 - 3
apps/app/src/components/Admin/App/FileUploadSetting.tsx

@@ -35,7 +35,7 @@ export const FileUploadSettingMolecule = React.memo((props: FileUploadSettingMol
         <br />
         <br />
         <span className="text-danger">
-          <i className="ti ti-unlink"></i>
+          <span className="material-symbols-outlined">link_off</span>
           {t('admin:app_setting.change_setting')}
         </span>
       </p>
@@ -65,8 +65,8 @@ export const FileUploadSettingMolecule = React.memo((props: FileUploadSettingMol
         </div>
         {props.isFixedFileUploadByEnvVar && (
           <p className="alert alert-warning mt-2 text-start offset-3 col-6">
-            <i className="icon-exclamation icon-fw">
-            </i><b>FIXED</b><br />
+            <span className="material-symbols-outlined">help</span>
+            <b>FIXED</b><br />
             {/* eslint-disable-next-line react/no-danger */}
             <b dangerouslySetInnerHTML={{ __html: t('admin:app_setting.fixed_by_env_var', { fileUploadType: props.envFileUploadType }) }} />
           </p>

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

@@ -47,7 +47,7 @@ const MailSetting = (props: Props) => {
   return (
     <React.Fragment>
       {!adminAppContainer.state.isMailerSetup && (
-        <div className="alert alert-danger"><i className="icon-exclamation"></i> {t('admin:app_setting.mailer_is_not_set_up')}</div>
+        <div className="alert alert-danger"><span className="material-symbols-outlined">error</span> {t('admin:app_setting.mailer_is_not_set_up')}</div>
       )}
       <div className="row mb-5">
         <label className="col-md-3 col-form-label text-end">{t('admin:app_setting.from_e-mail_address')}</label>

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

@@ -59,7 +59,7 @@ export const MaintenanceMode: FC = () => {
         <br />
         <br />
         <span className="text-warning">
-          <i className="icon-exclamation icon-fw"></i>
+          <span className="material-symbols-outlined">error</span>
           {t('admin:maintenance_mode.supplymentary_message_to_start')}
         </span>
       </p>

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

@@ -53,9 +53,9 @@ const QuestionnaireSettings = (): JSX.Element => {
         <div className="mb-4">{t('app_setting.questionnaire_settings_explanation')}</div>
         <span>
           <div className="mb-2">
-            <span className="text-info me-2"><i className="icon-info icon-fw"></i>{t('app_setting.about_data_sent')}</span>
+            <span className="text-info me-2"><span className="material-symbols-outlined">info</span>{t('app_setting.about_data_sent')}</span>
             <a href={t('app_setting.docs_link')} rel="noreferrer" target="_blank" className="d-inline">
-              {t('app_setting.learn_more')} <i className="icon-share-alt"></i>
+              {t('app_setting.learn_more')} <span className="material-symbols-outlined">share</span>
             </a>
           </div>
           {t('app_setting.other_info_will_be_sent')}<br />

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

@@ -37,7 +37,7 @@ const SiteUrlSetting = (props: Props) => {
     <React.Fragment>
       <p className="card custom-card">{t('site_url.desc')}</p>
       {!adminAppContainer.state.isSetSiteUrl
-          && (<p className="alert alert-danger"><i className="icon-exclamation"></i> {t('site_url.warn')}</p>)}
+          && (<p className="alert alert-danger"><span className="material-symbols-outlined">error</span> {t('site_url.warn')}</p>)}
 
       { adminAppContainer.state.siteUrlUseOnlyEnvVars && (
         <div className="row">

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

@@ -138,7 +138,7 @@ const V5PageMigration: FC<Props> = (props: Props) => {
         <br />
         <br />
         <span className="text-danger">
-          <i className="icon-exclamation icon-fw"></i>
+          <span className="material-symbols-outlined">error</span>
           {t('admin:v5_page_migration.migration_note')}
         </span>
       </p>

+ 2 - 1
apps/app/src/components/Admin/AuditLog/AuditLogDisableMode.tsx

@@ -11,7 +11,8 @@ export const AuditLogDisableMode: FC = () => {
         <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>
+              {/* error icon large */}
+              <h1><span className="material-symbols-outlined">error</span></h1>
               <h1 className="text-center">{t('audit_log_management.audit_log')}</h1>
               <h3
                 // eslint-disable-next-line react/no-danger

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

@@ -24,8 +24,8 @@ export const AuditLogSettings: FC = () => {
         {t('admin:audit_log_management.activity_expiration_date_explain')}
       </p>
       <p className="alert alert-warning col-6">
-        <i className="icon-exclamation icon-fw">
-        </i><b>FIXED</b><br />
+        <span className="material-symbols-outlined">error</span>
+        <b>FIXED</b><br />
         <b
           // eslint-disable-next-line react/no-danger
           dangerouslySetInnerHTML={{
@@ -46,7 +46,7 @@ export const AuditLogSettings: FC = () => {
           target="_blank"
           rel="noopener noreferrer"
         >
-          <i className="icon-fw icon-question" aria-hidden="true"></i>
+          <span className="material-symbols-outlined" aria-hidden="true">help</span>
         </a>
       </h4>
       <p className="form-text text-muted">

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

@@ -111,7 +111,7 @@ const SearchUsernameTypeaheadSubstance: ForwardRefRenderFunction<IClearable, Pro
   return (
     <div className="input-group me-2">
       <span className="input-group-text">
-        <i className="icon-people" />
+        <span className="material-symbols-outlined">person</span>
       </span>
       <AsyncTypeahead
         ref={typeaheadRef}

+ 3 - 3
apps/app/src/components/Admin/AuditLogManagement.tsx

@@ -152,8 +152,8 @@ export const AuditLogManagement: FC = () => {
       <button type="button" className="btn btn-outline-secondary mb-4" onClick={() => setIsSettingPage(!isSettingPage)}>
         {
           isSettingPage
-            ? <><i className="fa fa-hand-o-left me-1" />{t('admin:audit_log_management.return')}</>
-            : <><i className="fa icon-settings me-1" />{t('admin:audit_log_management.settings')}</>
+            ? <><span className="material-symbols-outlined">arrow_left_alt</span>{t('admin:audit_log_management.return')}</>
+            : <><span className="material-symbols-outlined">settings</span>{t('admin:audit_log_management.settings')}</>
         }
       </button>
 
@@ -163,7 +163,7 @@ export const AuditLogManagement: FC = () => {
         </span>
         { !isSettingPage && (
           <button type="button" className="btn btn-sm ms-auto grw-btn-reload" onClick={reloadButtonPushedHandler}>
-            <i className="icon icon-reload"></i>
+            <span className="material-symbols-outlined">refresh</span>
           </button>
         )}
       </h2>

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

@@ -13,23 +13,23 @@ const MenuLabel = ({ menu }: { menu: string }) => {
 
   switch (menu) {
     /* eslint-disable no-multi-spaces, max-len */
-    case 'app':                      return <><i className="me-1 icon-fw icon-settings"></i>{        t('headers.app_settings', { ns: 'commons' }) }</>;
-    case 'security':                 return <><i className="me-1 icon-fw icon-shield"></i>{          t('security_settings.security_settings') }</>;
-    case 'markdown':                 return <><i className="me-1 icon-fw icon-note"></i>{            t('markdown_settings.markdown_settings') }</>;
-    case 'customize':                return <><i className="me-1 icon-fw icon-wrench"></i>{          t('customize_settings.customize_settings') }</>;
-    case 'importer':                 return <><i className="me-1 icon-fw icon-cloud-upload"></i>{    t('importer_management.import_data') }</>;
-    case 'export':                   return <><i className="me-1 icon-fw icon-cloud-download"></i>{  t('export_management.export_archive_data') }</>;
-    case 'data-transfer':            return <><i className="me-1 icon-fw icon-plane"></i>{           t('g2g_data_transfer.data_transfer', { ns: 'commons' })}</>;
-    case 'notification':             return <><i className="me-1 icon-fw icon-bell"></i>{            t('external_notification.external_notification')}</>;
-    case 'slack-integration':        return <><i className="me-1 icon-fw icon-shuffle"></i>{         t('slack_integration.slack_integration') }</>;
-    case 'slack-integration-legacy': return <><i className="me-1 icon-fw icon-shuffle"></i>{         t('slack_integration_legacy.slack_integration_legacy')}</>;
-    case 'users':                    return <><i className="me-1 icon-fw icon-user"></i>{            t('user_management.user_management') }</>;
-    case 'user-groups':              return <><i className="me-1 icon-fw icon-people"></i>{          t('user_group_management.user_group_management') }</>;
-    case 'audit-log':                return <><i className="me-1 icon-fw icon-feed"></i>{            t('audit_log_management.audit_log')}</>;
-    case 'plugins':                  return <><i className="me-1 icon-fw icon-puzzle"></i>{          t('plugins.plugins')}</>;
-    case 'search':                   return <><i className="me-1 icon-fw icon-magnifier"></i>{       t('full_text_search_management.full_text_search_management') }</>;
-    case 'cloud':                    return <><i className="me-1 icon-fw icon-share-alt"></i>{       t('cloud_setting_management.to_cloud_settings')} </>;
-    default:                         return <><i className="me-1 icon-fw icon-home"></i>{            t('wiki_management_homepage') }</>;
+    case 'app':                      return <><span className="material-symbols-outlined me-1">settings</span>{        t('headers.app_settings', { ns: 'commons' }) }</>;
+    case 'security':                 return <><span className="material-symbols-outlined me-1">shield</span>{          t('security_settings.security_settings') }</>;
+    case 'markdown':                 return <><span className="material-symbols-outlined me-1">note</span>{            t('markdown_settings.markdown_settings') }</>;
+    case 'customize':                return <><span className="material-symbols-outlined me-1">construction</span>{          t('customize_settings.customize_settings') }</>;
+    case 'importer':                 return <><span className="material-symbols-outlined me-1">cloud_upload</span>{    t('importer_management.import_data') }</>;
+    case 'export':                   return <><span className="material-symbols-outlined me-1">cloud_download</span>{  t('export_management.export_archive_data') }</>;
+    case 'data-transfer':            return <><span className="material-symbols-outlined me-1">flight</span>{           t('g2g_data_transfer.data_transfer', { ns: 'commons' })}</>;
+    case 'notification':             return <><span className="material-symbols-outlined me-1">notifications</span>{            t('external_notification.external_notification')}</>;
+    case 'slack-integration':        return <><span className="material-symbols-outlined me-1">shuffle</span>{         t('slack_integration.slack_integration') }</>;
+    case 'slack-integration-legacy': return <><span className="material-symbols-outlined me-1">shuffle</span>{         t('slack_integration_legacy.slack_integration_legacy')}</>;
+    case 'users':                    return <><span className="material-symbols-outlined me-1">person</span>{            t('user_management.user_management') }</>;
+    case 'user-groups':              return <><span className="material-symbols-outlined me-1">group</span>{          t('user_group_management.user_group_management') }</>;
+    case 'audit-log':                return <><span className="material-symbols-outlined me-1">feed</span>{            t('audit_log_management.audit_log')}</>;
+    case 'plugins':                  return <><span className="material-symbols-outlined me-1">extension</span>{          t('plugins.plugins')}</>;
+    case 'search':                   return <><span className="material-symbols-outlined me-1">search</span>{       t('full_text_search_management.full_text_search_management') }</>;
+    case 'cloud':                    return <><span className="material-symbols-outlined me-1">share</span>{       t('cloud_setting_management.to_cloud_settings')} </>;
+    default:                         return <><span className="material-symbols-outlined me-1">home</span>{            t('wiki_management_homepage') }</>;
       /* eslint-enable no-multi-spaces, max-len */
   }
 };

+ 2 - 2
apps/app/src/components/Admin/ElasticsearchManagement/StatusTable.jsx

@@ -56,7 +56,7 @@ class StatusTable extends React.PureComponent {
     const aliasLabels = aliases.map((aliasName) => {
       return (
         <span key={`badge-${indexName}-${aliasName}`} className="badge rounded-pill bg-primary me-2">
-          <i className="icon-tag"></i> {aliasName}
+          <span className="material-symbols-outlined">sell</span> {aliasName}
         </span>
       );
     });
@@ -66,7 +66,7 @@ class StatusTable extends React.PureComponent {
         <div className="card-header">
 
           <a role="button" className="text-nowrap me-2" data-bs-toggle="collapse" href={`#${collapseId}`} aria-expanded="true" aria-controls={collapseId}>
-            <i className="fa fa-fw fa-database"></i> {indexName}
+            <span className="material-symbols-outlined">database</span> {indexName}
           </a>
           <span className="ms-md-3">{aliasLabels}</span>
         </div>

+ 3 - 3
apps/app/src/components/Admin/ExportArchiveData/ArchiveFilesTableMenu.tsx

@@ -15,15 +15,15 @@ const ArchiveFilesTableMenu = (props: ArchiveFilesTableMenuProps):JSX.Element =>
   return (
     <div className="btn-group admin-user-menu dropdown">
       <button type="button" className="btn btn-sm btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown">
-        <i className="icon-settings"></i> <span className="caret"></span>
+        <span className="material-symbols-outlined">settings</span> <span className="caret"></span>
       </button>
       <ul className="dropdown-menu" role="menu">
         <li className="dropdown-header">{t('admin:export_management.export_menu')}</li>
         <button type="button" className="dropdown-item" onClick={() => { window.location.href = `/admin/export/${props.fileName}` }}>
-          <i className="icon-cloud-download" /> {t('admin:export_management.download')}
+          <span className="material-symbols-outlined">cloud_download</span> {t('admin:export_management.download')}
         </button>
         <button type="button" className="dropdown-item" role="button" onClick={() => props.onZipFileStatRemove(props.fileName)}>
-          <span className="text-danger"><i className="icon-trash" /> {t('admin:export_management.delete')}</span>
+          <span className="text-danger"><span className="material-symbols-outlined">delete</span> {t('admin:export_management.delete')}</span>
         </button>
       </ul>
     </div>

+ 1 - 1
apps/app/src/components/Admin/ImportData/GrowiArchive/ImportCollectionItem.jsx

@@ -151,7 +151,7 @@ export default class ImportCollectionItem extends React.Component {
         disabled={isImporting || !isConfigButtonAvailable}
         onClick={isConfigButtonAvailable ? this.configButtonClickedHandler : null}
       >
-        <i className="icon-settings"></i>
+        <span className="material-symbols-outlined">settings</span>
       </button>
     );
   }

+ 3 - 3
apps/app/src/components/Admin/ImportData/ImportDataPageContents.jsx

@@ -41,12 +41,12 @@ class ImportDataPageContents extends React.Component {
               <tbody>
                 <tr>
                   <th>{t('importer_management.article')}</th>
-                  <th><i className="icon-arrow-right-circle text-success"></i></th>
+                  <th><span className="material-symbols-outlined text-success">arrow_circle_right</span></th>
                   <th>{t('importer_management.page')}</th>
                 </tr>
                 <tr>
                   <th>{t('importer_management.category')}</th>
-                  <th><i className="icon-arrow-right-circle text-success"></i></th>
+                  <th><span className="material-symbols-outlined text-success">arrow_circle_right</span></th>
                   <th>{t('importer_management.page_path')}</th>
                 </tr>
                 <tr>
@@ -143,7 +143,7 @@ class ImportDataPageContents extends React.Component {
               <tbody>
                 <tr>
                   <th>{t('importer_management.article')}</th>
-                  <th><i className="icon-arrow-right-circle text-success"></i></th>
+                  <th><span className="material-symbols-outlined">arrow_circle_right</span></th>
                   <th>{t('importer_management.page')}</th>
                 </tr>
                 <tr>

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

@@ -42,14 +42,14 @@ const LegacySlackIntegration = (props) => {
     <div data-testid="admin-slack-integration-legacy">
       { isDisabled && (
         <div className="alert alert-danger">
-          <i className="icon-minus icon-fw"></i>
+          <span className="material-symbols-outlined">remove</span>
           {/* eslint-disable-next-line react/no-danger */}
           <span dangerouslySetInnerHTML={{ __html: t('admin:slack_integration_legacy.alert_disabled') }}></span>
         </div>
       ) }
 
       <div className="alert alert-warning">
-        <i className="icon-info icon-fw"></i>
+        <span className="material-symbols-outlined">info</span>
         {/* eslint-disable-next-line react/no-danger */}
         <span dangerouslySetInnerHTML={{ __html: t('admin:slack_integration_legacy.alert_deplicated') }}></span>
       </div>

+ 2 - 2
apps/app/src/components/Admin/LegacySlackIntegration/SlackConfiguration.jsx

@@ -102,7 +102,7 @@ class SlackConfiguration extends React.Component {
               <h2 className="border-bottom mb-5">{t('notification_settings.slack_app_configuration')}</h2>
 
               <div className="card custom-card">
-                <span className="text-danger"><i className="icon-fw icon-exclamation"></i>NOT RECOMMENDED</span>
+                <span className="text-danger"><span className="material-symbols-outlined">error</span>NOT RECOMMENDED</span>
                 <br />
                 {/* eslint-disable-next-line react/no-danger */}
                 <span dangerouslySetInnerHTML={{ __html: t('notification_settings.slack_app_configuration_desc') }} />
@@ -140,7 +140,7 @@ class SlackConfiguration extends React.Component {
         <hr />
 
         <h3>
-          <i className="icon-question" aria-hidden="true"></i>{' '}
+          <span className="material-symbols-outlined" aria-hidden="true">help</span>{' '}
           <a href="#collapseHelpForIwh" data-bs-toggle="collapse">{t('notification_settings.how_to.header')}</a>
         </h3>
 

+ 1 - 1
apps/app/src/components/Admin/ManageExternalAccount.tsx

@@ -54,7 +54,7 @@ const ManageExternalAccount = (props: ManageExternalAccountProps): JSX.Element =
           prefetch={false}
           className="btn btn-outline-secondary"
         >
-          <i className="icon-fw ti ti-arrow-left" aria-hidden="true"></i>
+          <span className="material-symbols-outlined" aria-hidden="true">arrow_back</span>
           {t('admin:user_management.back_to_user_management')}
         </Link>
       </p>

+ 9 - 10
apps/app/src/components/Admin/Notification/GlobalNotificationList.jsx

@@ -98,32 +98,32 @@ class GlobalNotificationList extends React.Component {
                 <ul className="list-inline mb-0">
                   {notification.triggerEvents.includes('pageCreate') && (
                     <li className="list-inline-item badge rounded-pill bg-success">
-                      <i className="icon-doc"></i> CREATE
+                      <span className=" material-symbols-outlined">description</span> CREATE
                     </li>
                   )}
                   {notification.triggerEvents.includes('pageEdit') && (
                     <li className="list-inline-item badge rounded-pill bg-warning text-dark">
-                      <i className="icon-pencil"></i> EDIT
+                      <span className="material-symbols-outlined">edit</span> EDIT
                     </li>
                   )}
                   {notification.triggerEvents.includes('pageMove') && (
                     <li className="list-inline-item badge rounded-pill bg-pink">
-                      <i className="icon-action-redo"></i> MOVE
+                      <span className="material-symbols-outlined">redo</span> MOVE
                     </li>
                   )}
                   {notification.triggerEvents.includes('pageDelete') && (
                     <li className="list-inline-item badge rounded-pill bg-danger">
-                      <i className="icon-fire"></i> DELETE
+                      <span className="material-symbols-outlined">delete_forever</span>DELETE
                     </li>
                   )}
                   {notification.triggerEvents.includes('pageLike') && (
                     <li className="list-inline-item badge rounded-pill bg-info">
-                      <i className="fa fa-heart-o"></i> LIKE
+                      <span className="material-symbols-outlined">favorite</span> LIKE
                     </li>
                   )}
                   {notification.triggerEvents.includes('comment') && (
                     <li className="list-inline-item badge rounded-pill bg-primary">
-                      <i className="icon-fw icon-bubble"></i> POST
+                      <span className="material-symbols-outlined">bubble_chart</span> POST
                     </li>
                   )}
                 </ul>
@@ -143,14 +143,14 @@ class GlobalNotificationList extends React.Component {
                     aria-haspopup="true"
                     aria-expanded="false"
                   >
-                    <i className="icon-settings"></i> <span className="caret"></span>
+                    <span className="material-symbols-outlined">settings</span> <span className="caret"></span>
                   </button>
                   <div className="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
                     <a className="dropdown-item" href={urljoin('/admin/global-notification/', notification._id)}>
-                      <i className="icon-fw icon-note"></i> {t('Edit')}
+                      <span className="material-symbols-outlined">note</span> {t('Edit')}
                     </a>
                     <button className="dropdown-item" type="button" onClick={() => this.openConfirmationModal(notification)}>
-                      <i className="icon-fw icon-fire text-danger"></i> {t('Delete')}
+                      <span className="material-symbols-outlined text-danger">delete_forever</span> {t('Delete')}
                     </button>
                   </div>
                 </div>
@@ -168,7 +168,6 @@ class GlobalNotificationList extends React.Component {
         )}
       </React.Fragment>
     );
-
   }
 
 }

+ 10 - 10
apps/app/src/components/Admin/Notification/ManageGlobalNotification.tsx

@@ -113,7 +113,7 @@ const ManageGlobalNotification = (props: Props): JSX.Element => {
     <>
       <div className="my-3">
         <Link href="/admin/notification" className="btn btn-outline-secondary">
-          <i className="icon-fw ti ti-arrow-left" aria-hidden="true"></i>
+          <span className="material-symbols-outlined" aria-hidden="true">arrow_left_alt</span>
           {t('notification_settings.back_to_list')}
         </Link>
       </div>
@@ -179,7 +179,7 @@ const ManageGlobalNotification = (props: Props): JSX.Element => {
               <>
                 <div className="input-group notify-to-option" id="mail-input">
                   <div>
-                    <span className="input-group-text" id="mail-addon"><i className="ti ti-email" /></span>
+                    <span className="input-group-text" id="mail-addon"></span><span className="material-symbols-outlined">mail</span>
                   </div>
                   <input
                     className="form-control"
@@ -198,7 +198,7 @@ const ManageGlobalNotification = (props: Props): JSX.Element => {
                   {!isMailerSetup && <span className="form-text text-muted" dangerouslySetInnerHTML={{ __html: t('admin:mailer_setup_required') }} />}
                   <b>Hint: </b>
                   <a href="https://ifttt.com/create" target="blank">{t('notification_settings.email.ifttt_link')}
-                    <i className="icon-share-alt" />
+                    <span className="material-symbols-outlined">share</span>
                   </a>
                 </p>
               </>
@@ -207,7 +207,7 @@ const ManageGlobalNotification = (props: Props): JSX.Element => {
               <>
                 <div className="input-group notify-to-option" id="slack-input">
                   <div>
-                    <span className="input-group-text" id="slack-channel-addon"><i className="fa fa-hashtag" /></span>
+                    <span className="input-group-text" id="slack-channel-addon"></span><span className="material-symbols-outlined">tag</span>
                   </div>
                   <input
                     className="form-control"
@@ -238,7 +238,7 @@ const ManageGlobalNotification = (props: Props): JSX.Element => {
                 onChange={() => onChangeTriggerEvents(TriggerEventType.CREATE)}
               >
                 <span className="badge rounded-pill bg-success">
-                  <i className="icon-doc me-1" /> CREATE
+                  <span className="material-symbols-outlined">edit_note</span> CREATE
                 </span>
               </TriggerEventCheckBox>
             </div>
@@ -250,7 +250,7 @@ const ManageGlobalNotification = (props: Props): JSX.Element => {
                 onChange={() => onChangeTriggerEvents(TriggerEventType.EDIT)}
               >
                 <span className="badge rounded-pill bg-warning text-dark">
-                  <i className="icon-pencil me-1" />EDIT
+                  <span className="imaterial-symbols-outlined">edit</span> EDIT
                 </span>
               </TriggerEventCheckBox>
             </div>
@@ -262,7 +262,7 @@ const ManageGlobalNotification = (props: Props): JSX.Element => {
                 onChange={() => onChangeTriggerEvents(TriggerEventType.MOVE)}
               >
                 <span className="badge rounded-pill bg-pink">
-                  <i className="icon-action-redo me-1" />MOVE
+                  <span className="material-symbols-outlined">redo</span>MOVE
                 </span>
               </TriggerEventCheckBox>
             </div>
@@ -274,7 +274,7 @@ const ManageGlobalNotification = (props: Props): JSX.Element => {
                 onChange={() => onChangeTriggerEvents(TriggerEventType.DELETE)}
               >
                 <span className="badge rounded-pill bg-danger">
-                  <i className="icon-fire me-1" />DELETE
+                  <span className="material-symbols-outlined">delete_forever</span>DELETE
                 </span>
               </TriggerEventCheckBox>
             </div>
@@ -286,7 +286,7 @@ const ManageGlobalNotification = (props: Props): JSX.Element => {
                 onChange={() => onChangeTriggerEvents(TriggerEventType.LIKE)}
               >
                 <span className="badge rounded-pill bg-info">
-                  <i className="fa fa-heart-o me-1" />LIKE
+                  <span className="material-symbols-outlined">favorite</span>LIKE
                 </span>
               </TriggerEventCheckBox>
             </div>
@@ -298,7 +298,7 @@ const ManageGlobalNotification = (props: Props): JSX.Element => {
                 onChange={() => onChangeTriggerEvents(TriggerEventType.POST)}
               >
                 <span className="badge rounded-pill bg-primary">
-                  <i className="icon-bubble me-1" />POST
+                  <span className="material-symbols-outlined">language</span>POST
                 </span>
               </TriggerEventCheckBox>
             </div>

+ 2 - 2
apps/app/src/components/Admin/Notification/NotificationDeleteModal.jsx

@@ -13,7 +13,7 @@ class NotificationDeleteModal extends React.PureComponent {
     return (
       <Modal isOpen={this.props.isOpen} toggle={this.props.onClose}>
         <ModalHeader tag="h4" toggle={this.props.onClose} className="bg-danger text-light">
-          <i className="icon icon-fire"></i> Delete Global Notification Setting
+          <span className="material-symbols-outlined">delete_forever</span>Delete Global Notification Setting
         </ModalHeader>
         <ModalBody>
           <p>
@@ -25,7 +25,7 @@ class NotificationDeleteModal extends React.PureComponent {
         </ModalBody>
         <ModalFooter>
           <button type="button" className="btn btn-sm btn-danger" onClick={this.props.onClickSubmit}>
-            <i className="icon icon-fire"></i> {t('Delete')}
+            <span className="material-symbols-outlined">delete_forever</span> {t('Delete')}
           </button>
         </ModalFooter>
       </Modal>

+ 2 - 2
apps/app/src/components/Admin/Notification/NotificationSetting.jsx

@@ -124,11 +124,11 @@ function NotificationSetting(props) {
   const navTabMapping = useMemo(() => {
     return {
       user_trigger_notification: {
-        Icon: () => <i className="icon-settings" />,
+        Icon: () => <span className="material-symbols-outlined">settings</span>,
         i18n: 'User trigger notification',
       },
       global_notification: {
-        Icon: () => <i className="icon-settings" />,
+        Icon: () => <span className="material-symbols-outlined">settings</span>,
         i18n: 'Global notification',
       },
     };

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

@@ -22,7 +22,7 @@ const DeleteAllShareLinksModal = React.memo((props) => {
     <Modal isOpen={props.isOpen} toggle={closeButtonHandler} className="page-comment-delete-modal">
       <ModalHeader tag="h4" toggle={closeButtonHandler} className="bg-danger text-light">
         <span>
-          <i className="icon-fw icon-fire"></i>
+          <span className="material-symbols-outlined">delete_forever</span>
           {t('security_settings.delete_all_share_links')}
         </span>
       </ModalHeader>
@@ -32,7 +32,7 @@ const DeleteAllShareLinksModal = React.memo((props) => {
       <ModalFooter>
         <Button onClick={closeButtonHandler}>{t('Cancel')}</Button>
         <Button color="danger" onClick={deleteAllLinkHandler}>
-          <i className="icon icon-fire"></i>
+          <span className="material-symbols-outlined">delete_forever</span>
           {t('Delete')}
         </Button>
       </ModalFooter>

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

@@ -175,7 +175,7 @@ class GoogleSecurityManagementContents 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="#collapseHelpForGoogleOauth" data-bs-toggle="collapse"> {t('security_settings.OAuth.how_to.google')}</a>
           </h4>
           <ol id="collapseHelpForGoogleOauth" className="collapse">

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

@@ -449,7 +449,7 @@ class OidcSecurityManagementContents extends React.Component {
 
         <div style={{ minHeight: '300px' }}>
           <h4>
-            <i className="icon-question" aria-hidden="true" />
+            <span className="material-symbols-outlined" aria-hidden="true">help</span>
             <a href="#collapseHelpForOidcOauth" data-bs-toggle="collapse"> {t('security_settings.OAuth.how_to.oidc')}</a>
           </h4>
           <ol id="collapseHelpForOidcOauth" className="collapse">

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

@@ -468,8 +468,21 @@ class SecuritySetting extends React.Component {
                 {t('security_settings.user_homepage_deletion.enable_user_homepage_deletion')}
               </label>
             </div>
+            <div className="custom-control custom-switch custom-checkbox-success mt-2">
+              <input
+                type="checkbox"
+                className="form-check-input"
+                id="is-force-delete-user-homepage-on-user-deletion"
+                checked={adminGeneralSecurityContainer.state.isForceDeleteUserHomepageOnUserDeletion}
+                onChange={() => { adminGeneralSecurityContainer.switchIsForceDeleteUserHomepageOnUserDeletion() }}
+                disabled={!adminGeneralSecurityContainer.state.isUsersHomepageDeletionEnabled}
+              />
+              <label className="form-check-label" htmlFor="is-force-delete-user-homepage-on-user-deletion">
+                {t('security_settings.user_homepage_deletion.enable_force_delete_user_homepage_on_user_deletion')}
+              </label>
+            </div>
             <p
-              className="form-text text-muted small"
+              className="form-text text-muted small mt-2"
               dangerouslySetInnerHTML={{ __html: t('security_settings.user_homepage_deletion.desc') }}
             />
           </div>

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

@@ -24,13 +24,13 @@ const DeleteSlackBotSettingsModal = React.memo((props) => {
         <span>
           {props.isResetAll && (
             <>
-              <i className="icon-fw icon-fire" />
+              <span className="material-symbols-outlined">delete_forever</span>
               {t('admin:slack_integration.reset_all_settings')}
             </>
           )}
           {!props.isResetAll && (
             <>
-              <i className="icon-trash me-1" />
+              <span className="material-symbols-outlined">delete</span>
               {t('admin:slack_integration.delete_slackbot_settings')}
             </>
           )}
@@ -55,13 +55,13 @@ const DeleteSlackBotSettingsModal = React.memo((props) => {
         <Button color="danger" onClick={deleteSlackCredentialsHandler}>
           {props.isResetAll && (
             <>
-              <i className="icon icon-fire"></i>
+              <span className="material-symbols-outlined">delete_forever</span>
               {t('admin:slack_integration.reset')}
             </>
           )}
           {!props.isResetAll && (
             <>
-              <i className="icon-trash me-1" />
+              <span className="material-symbols-outlined">delete</span>
               {t('admin:slack_integration.delete')}
             </>
           )}

+ 1 - 1
apps/app/src/components/Admin/SlackIntegration/SlackAppIntegrationControl.tsx

@@ -46,7 +46,7 @@ export const SlackAppIntegrationControl: FC<Props> = (props: Props) => {
           }
         }}
       >
-        <i className="icon-trash me-1" />
+        <span className="material-symbols-outlined">delete</span>
         {t('admin:slack_integration.delete')}
       </button>
     </div>

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

@@ -184,7 +184,7 @@ export const UserGroupDeleteModal: FC<Props> = (props: Props) => {
   return (
     <Modal className="modal-md" isOpen={props.isShow} toggle={toggleHandler}>
       <ModalHeader tag="h4" toggle={toggleHandler} className="bg-danger text-light">
-        <i className="icon icon-fire"></i> {t('admin:user_group_management.delete_modal.header')}
+        <span className="material-symbols-outlined">delete_forever</span> {t('admin:user_group_management.delete_modal.header')}
       </ModalHeader>
       <ModalBody>
         <div>
@@ -201,7 +201,7 @@ export const UserGroupDeleteModal: FC<Props> = (props: Props) => {
             {renderGroupSelector()}
           </div>
           <button type="submit" value="" className="btn btn-sm btn-danger text-nowrap" disabled={!validateForm()}>
-            <i className="icon icon-fire"></i> {t('Delete')}
+            <span className="material-symbols-outlined">delete_forever</span> {t('Delete')}
           </button>
         </form>
       </ModalFooter>

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

@@ -219,7 +219,7 @@ export const UserGroupTable: FC<Props> = ({
                             </button>
                           )}
                           <button className="dropdown-item" type="button" role="button" onClick={onClickDelete} data-user-group-id={group._id}>
-                            <i className="icon-fw icon-fire text-danger"></i> {t('Delete')}
+                            <span className="material-symbols-outlined text-danger">delete_forever</span> {t('Delete')}
                           </button>
                         </div>
                       </div>

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

@@ -128,7 +128,7 @@ const UserManagement = (props: UserManagementProps) => {
           className="btn btn-outline-secondary ms-2"
           role="button"
         >
-          <i className="icon-user-follow me-1" aria-hidden="true"></i>
+          <span className="material-symbols-outlined" aria-hidden="true">person_add</span>
           {t('admin:user_management.external_account')}
         </Link>
       </p>
@@ -138,7 +138,7 @@ const UserManagement = (props: UserManagementProps) => {
 
         <div className="row d-flex justify-content-start align-items-center my-2">
           <div className="col-md-3 d-flex align-items-center my-2">
-            <i className="icon-magnifier me-1"></i>
+            <span className="material-symbols-outlined">search</span>
             <span className={`search-typeahead ${styles['search-typeahead']}`}>
               <input
                 className="w-100"
@@ -183,7 +183,7 @@ const UserManagement = (props: UserManagementProps) => {
               className="btn btn-outline-secondary btn-sm"
               onClick={resetButtonClickHandler}
             >
-              <span className="icon-refresh me-1"></span>
+              <span className="material-symbols-outlined">refresh</span>
               {t('commons:Reset')}
             </button>
           </div>

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

@@ -102,7 +102,7 @@ const ExternalAccountTable = (props: ExternalAccountTableProps): JSX.Element =>
                         role="button"
                         onClick={() => removeExtenalAccount(ea._id)}
                       >
-                        <i className="icon-fw icon-fire text-danger"></i> {t('Delete')}
+                        <span className="material-symbols-outlined text-danger">delete_forever</span> {t('Delete')}
                       </button>
                     </ul>
                   </div>

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

@@ -34,7 +34,7 @@ class UserRemoveButton extends React.Component {
 
     return (
       <button className="dropdown-item" type="button" onClick={() => { this.onClickDeleteBtn() }}>
-        <i className="icon-fw icon-fire text-danger"></i> {t('Delete')}
+        <span className="material-symbols-outlined text-danger">delete_forever</span> {t('Delete')}
       </button>
     );
   }

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

@@ -28,10 +28,10 @@ export const AlertSiteUrlUndefined = (): JSX.Element => {
 
   return (
     <div className="alert alert-danger rounded-0 d-edit-none mb-0 px-4 py-2">
-      <i className="icon-exclamation"></i>
+      <span className="material-symbols-outlined">error</span>
       {
         t('alert.siteUrl_is_not_set', { link: t('headers.app_settings') })
-      } &gt;&gt; <a href="/admin/app">{t('headers.app_settings')}<i className="icon-login"></i></a>
+      } &gt;&gt; <a href="/admin/app">{t('headers.app_settings')}<span className="material-symbols-outlined">login</span></a>
     </div>
   );
 };

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

@@ -266,7 +266,7 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
               >
                 <div onClick={e => e.stopPropagation()}>
                   <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control p-0 grw-visible-on-hover me-1">
-                    <i className="icon-options fa fa-rotate-90 p-1"></i>
+                    <span className="material-symbols-outlined">more_vert</span>
                   </DropdownToggle>
                 </div>
               </BookmarkFolderItemControl>
@@ -278,7 +278,7 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
                   className="border-0 rounded btn btn-page-item-control p-0 grw-visible-on-hover"
                   onClick={onClickPlusButton}
                 >
-                  <i className="icon-plus d-block p-0" />
+                  <span className="material-symbols-outlined">add_circle</span>
                 </button>
               )}
             </div>

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

@@ -53,7 +53,7 @@ export const BookmarkFolderItemControl: React.FC<{
           className="pt-2 grw-page-control-dropdown-item text-danger"
           onClick={onClickDelete}
         >
-          <i className="icon-fw icon-trash grw-page-control-dropdown-icon"></i>
+          <span className="material-symbols-outlined grw-page-control-dropdown-icon">delete</span>
           {t('Delete')}
         </DropdownItem>
       </DropdownMenu>

+ 3 - 0
apps/app/src/components/Common/ClosableTextInput.tsx

@@ -12,6 +12,7 @@ type ClosableTextInputProps = {
   validationTarget?: string,
   onPressEnter?(inputText: string | null): void
   onClickOutside?(): void
+  handleInputChange?: (string) => void
 }
 
 const ClosableTextInput: FC<ClosableTextInputProps> = memo((props: ClosableTextInputProps) => {
@@ -38,6 +39,8 @@ const ClosableTextInput: FC<ClosableTextInputProps> = memo((props: ClosableTextI
     createValidation(inputText);
     setInputText(inputText);
     setIsAbleToShowAlert(true);
+
+    props.handleInputChange?.(inputText);
   };
 
   const onFocusHandler = async(e: React.ChangeEvent<HTMLInputElement>) => {

+ 37 - 0
apps/app/src/components/Common/Dropdown/PageItemControl.spec.tsx

@@ -0,0 +1,37 @@
+import { render, screen, waitFor } from '@testing-library/react';
+import userEvent, { PointerEventsCheckLevel } from '@testing-library/user-event';
+
+import { PageItemControl } from './PageItemControl';
+
+
+describe('PageItemControl.tsx', () => {
+  it('Should trigger onClickRenameMenuItem() when clicking the rename button with pageInfo.isDeletable being "false"', async() => {
+    // setup
+    const onClickRenameMenuItemMock = vi.fn();
+
+    const pageInfo = {
+      isMovable: true,
+      isV5Compatible: true,
+      isEmpty: false,
+      isDeletable: false,
+      isAbleToDeleteCompletely: true,
+      isRevertible: true,
+    };
+
+    const props = {
+      pageId: 'dummy-page-id',
+      isEnableActions: true,
+      pageInfo,
+      onClickRenameMenuItem: onClickRenameMenuItemMock,
+    };
+
+    render(<PageItemControl {...props} />);
+
+    // when
+    const openPageMoveRenameModalButton = screen.getByTestId('open-page-move-rename-modal-btn');
+    await waitFor(() => userEvent.click(openPageMoveRenameModalButton, { pointerEventsCheck: PointerEventsCheckLevel.Never }));
+
+    // then
+    expect(onClickRenameMenuItemMock).toHaveBeenCalled();
+  });
+});

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

@@ -230,7 +230,7 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
 
         {/* divider */}
         {/* Delete */}
-        { !forceHideMenuItems?.includes(MenuItemType.DELETE) && isEnableActions && !isReadOnlyUser && pageInfo.isMovable && (
+        { !forceHideMenuItems?.includes(MenuItemType.DELETE) && isEnableActions && !isReadOnlyUser && pageInfo.isDeletable && (
           <>
             { showDeviderBeforeDelete && <DropdownItem divider /> }
             <DropdownItem

+ 1 - 1
apps/app/src/components/Common/PagePathHierarchicalLink/PagePathHierarchicalLink.tsx

@@ -41,7 +41,7 @@ export const PagePathHierarchicalLink = memo((props: PagePathHierarchicalLinkPro
         <RootElm>
           <span className="path-segment">
             <Link href="/trash" prefetch={false}>
-              <i className="icon-trash"></i>
+              <span className="material-symbols-outlined">delete</span>
             </Link>
           </span>
           <span className={`separator ${styles.separator}`}><a href="/">/</a></span>

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

@@ -45,6 +45,11 @@ $page-view-layout-margin-top: 32px;
       min-width: 250px;
       margin-left: 30px;
     }
+
+    @include bs.media-breakpoint-down(sm) {
+      position: fixed;
+      right: 1rem;
+    }
   }
 }
 

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

@@ -14,7 +14,7 @@ export const CompleteUserRegistration: FC = () => {
           </p>
           {/* 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">login</span>{t('Sign in is here')}
           </a>
         </div>
       </div>

+ 6 - 6
apps/app/src/components/CompleteUserRegistrationForm.tsx

@@ -111,12 +111,12 @@ const CompleteUserRegistrationForm: React.FC<Props> = (props: Props) => {
               <input type="hidden" name="token" value={token} />
 
               <div className="input-group">
-                <span className="input-group-text"><i className="icon-envelope"></i></span>
+                <span className="input-group-text"></span><span className="material-symbols-outlined">mail</span>
                 <input type="text" className="form-control" placeholder={t('Email')} disabled value={email} />
               </div>
 
               <div className="input-group" id="input-group-username">
-                <span className="input-group-text"><i className="icon-user"></i></span>
+                <span className="input-group-text"></span><span className="material-symbols-outlined">person</span>
                 <input
                   type="text"
                   className="form-control"
@@ -129,12 +129,12 @@ const CompleteUserRegistrationForm: React.FC<Props> = (props: Props) => {
               </div>
               {!usernameAvailable && (
                 <p className="form-text text-red">
-                  <span id="help-block-username"><i className="icon-fw icon-ban"></i>{t('installer.unavaliable_user_id')}</span>
+                  <span id="help-block-username"><span className="material-symbols-outlined">block</span>{t('installer.unavaliable_user_id')}</span>
                 </p>
               )}
 
               <div className="input-group">
-                <span className="input-group-text"><i className="icon-tag"></i></span>
+                <span className="input-group-text"></span><span className="material-symbols-outlined">sell</span>
                 <input
                   type="text"
                   className="form-control"
@@ -148,7 +148,7 @@ const CompleteUserRegistrationForm: React.FC<Props> = (props: Props) => {
               </div>
 
               <div className="input-group">
-                <span className="input-group-text"><i className="icon-lock"></i></span>
+                <span className="input-group-text"></span><span className="material-symbols-outlined">lock</span>
                 <input
                   type="password"
                   className="form-control"
@@ -164,7 +164,7 @@ const CompleteUserRegistrationForm: React.FC<Props> = (props: Props) => {
               <div className="input-group justify-content-center d-flex mt-5">
                 <button type="button" disabled={forceDisableForm || disableForm} className="btn btn-fill" id="register">
                   <div className="eff"></div>
-                  <span className="btn-label"><i className="icon-user-follow"></i></span>
+                  <span className="btn-label"></span><span className="material-symbols-outlined">person_add</span>
                   <span className="btn-label-text">{t('Create')}</span>
                 </button>
               </div>

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

@@ -15,7 +15,7 @@ const BookMarkLinkButton = React.memo(() => {
         type="button"
         className="btn btn-outline-secondary btn-sm px-2"
       >
-        <i className="fa fa-fw fa-bookmark-o"></i>
+        <span className="material-symbols-outlined">bookmark</span>
         <span>Bookmarks</span>
       </button>
     </ScrollLink>

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

@@ -42,7 +42,7 @@ const DeleteBookmarkFolderModal: FC = () => {
   return (
     <Modal size="md" isOpen={isOpened} toggle={closeBookmarkFolderDeleteModal} data-testid="page-delete-modal" className="grw-create-page">
       <ModalHeader tag="h4" toggle={closeBookmarkFolderDeleteModal} className="bg-danger text-light">
-        <i className="icon-fw icon-trash"></i>
+        <span className="material-symbols-outlined">delete</span>
         {t('bookmark_folder.delete_modal.modal_header_label')}
       </ModalHeader>
       <ModalBody>
@@ -58,7 +58,7 @@ const DeleteBookmarkFolderModal: FC = () => {
           className="btn btn-danger"
           onClick={onClickDeleteButton}
         >
-          <i className="me-1 icon-trash" aria-hidden="true"></i>
+          <span className="material-symbols-outlined" aria-hidden="true">delete</span>
           {t('bookmark_folder.delete_modal.modal_footer_button')}
         </button>
       </ModalFooter>

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

@@ -24,7 +24,7 @@ const EmptyTrashButton = (props: EmptyTrashButtonProps): JSX.Element => {
         disabled={disableEmptyButton}
         onClick={emptyTrashButtonHandler}
       >
-        <i className="icon-fw icon-trash"></i>
+        <span className="material-symbols-outlined">delete</span>
         <div>{t('modal_empty.empty_the_trash')}</div>
       </button>
     </div>

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

@@ -61,7 +61,7 @@ const EmptyTrashModal: FC = () => {
   return (
     <Modal size="lg" isOpen={isOpened} toggle={closeEmptyTrashModal} data-testid="page-delete-modal">
       <ModalHeader tag="h4" toggle={closeEmptyTrashModal} className="bg-danger text-light">
-        <i className="icon-fw icon-fire"></i>
+        <span className="material-symbols-outlined">delete_forever</span>
         {t('modal_empty.empty_the_trash')}
       </ModalHeader>
       <ModalBody>
@@ -80,7 +80,7 @@ const EmptyTrashModal: FC = () => {
           className="btn btn-danger"
           onClick={emptyTrashButtonHandler}
         >
-          <i className="me-1 icon-fire" aria-hidden="true"></i>
+          <span className="material-symbols-outlined" aria-hidden="true">delete_forever</span>
           {t('modal_empty.empty_the_trash_button')}
         </button>
       </ModalFooter>

+ 3 - 1
apps/app/src/components/FontFamily/GlobalFonts.tsx

@@ -1,5 +1,6 @@
 import { memo } from 'react';
 
+import { useGrowiCustomIcon } from './use-growi-custom-icons';
 import { useLatoFontFamily } from './use-lato';
 import { useMaterialSymbolsOutlined } from './use-material-symbols-outlined';
 import { useSourceHanCodeJP } from './use-source-han-code-jp';
@@ -8,16 +9,17 @@ import { useSourceHanCodeJP } from './use-source-han-code-jp';
  * Define prefixed by '--grw-font-family'
  */
 export const GlobalFonts = memo((): JSX.Element => {
-
   const latoFontFamily = useLatoFontFamily();
   const sourceHanCodeJPFontFamily = useSourceHanCodeJP();
   const materialSymbolsOutlinedFontFamily = useMaterialSymbolsOutlined();
+  const customSvgFontFamily = useGrowiCustomIcon();
 
   return (
     <>
       {latoFontFamily}
       {sourceHanCodeJPFontFamily}
       {materialSymbolsOutlinedFontFamily}
+      {customSvgFontFamily}
     </>
   );
 });

+ 17 - 0
apps/app/src/components/FontFamily/use-growi-custom-icons.tsx

@@ -0,0 +1,17 @@
+import localFont from 'next/font/local';
+
+import { DefineStyle } from './types';
+
+const growiCustomIconFont = localFont({
+  src: '../../../../../packages/custom-icons/dist/growi-custom-icons.woff2',
+});
+
+export const useGrowiCustomIcon: DefineStyle = () => (
+  <style jsx global>
+    {`
+      :root {
+        --grw-font-family-custom-icon: ${growiCustomIconFont.style.fontFamily};
+      }
+    `}
+  </style>
+);

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

@@ -14,7 +14,7 @@ const ForbiddenPage = React.memo((props: Props): JSX.Element => {
       <div className="row not-found-message-row mb-4">
         <div className="col-lg-12">
           <h2 className="text-muted">
-            <i className="icon-ban me-2" aria-hidden="true" />
+            <span className="material-symbols-outlined" aria-hidden="true">block</span>
             Forbidden
           </h2>
         </div>
@@ -23,7 +23,7 @@ const ForbiddenPage = React.memo((props: Props): JSX.Element => {
       <div className="row row-alerts d-edit-none">
         <div className="col-sm-12">
           <p className="alert alert-primary py-3 px-4">
-            <i className="icon-fw icon-lock" aria-hidden="true" />
+            <span className="material-symbols-outlined" aria-hidden="true">lock</span>
             { props.isLinkSharingDisabled ? t('share_links.link_sharing_is_disabled') : t('Browsing of this page is restricted')}
           </p>
         </div>

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

@@ -1,20 +0,0 @@
-import React from 'react';
-
-const SidebarDockIcon = () => (
-  <svg
-    xmlns="http://www.w3.org/2000/svg"
-    viewBox="0 0 23 23"
-  >
-    <rect width="23" height="23" fillOpacity="0" />
-    <path
-      d="M20.86,3.92a.64.64,0,0,1,.64.63v13.9a.64.64,0,0,1-.64.63H2.14a.64.64,0,0,
-      1-.64-.63V4.55a.64.64,0,0,1,.64-.63H20.86m0-1.5H2.14A2.13,2.13,0,0,0,0,4.55v13.9a2.13,
-      2.13,0,0,0,2.14,2.13H20.86A2.13,2.13,0,0,0,23,18.45V4.55a2.13,2.13,0,0,0-2.14-2.13Z"
-    />
-    <rect x="7.49" y="3.05" width="1.2" height="16.91" />
-  </svg>
-
-);
-
-
-export default SidebarDockIcon;

+ 0 - 25
apps/app/src/components/Icons/SidebarDrawerIcon.jsx

@@ -1,25 +0,0 @@
-import React from 'react';
-
-const SidebarDrawerIcon = () => (
-  <svg
-    xmlns="http://www.w3.org/2000/svg"
-    viewBox="0 0 23 23"
-  >
-    <rect width="23" height="23" fillOpacity="0" />
-    <path d="M20.9,3.9c0.3,0,0.6,0.3,0.6,0.6v13.9c0,0.3-0.3,0.6-0.6,0.6H2.1c-0.3,0-0.6-0.3-0.6-0.6V4.5c0-0.3,0.3-0.6,0.6-0.6H20.9
-      M20.9,2.4H2.1C1,2.4,0,3.4,0,4.5c0,0,0,0,0,0v13.9c0,1.2,1,2.1,2.1,2.1c0,0,0,0,0,0h18.7c1.2,0,2.1-0.9,2.1-2.1c0,0,0,0,0,0V4.5
-      C23,3.4,22,2.4,20.9,2.4C20.9,2.4,20.9,2.4,20.9,2.4z"
-    />
-    <rect x="7.5" y="3.9" width="1.2" height="0.8" />
-    <rect x="7.5" y="15.3" width="1.2" height="1.5" />
-    <rect x="7.5" y="12.3" width="1.2" height="1.5" />
-    <rect x="7.5" y="9.2" width="1.2" height="1.6" />
-    <rect x="7.5" y="6.1" width="1.2" height="1.6" />
-    <rect x="7.5" y="18.4" width="1.2" height="0.8" />
-    <path d="M15.1,14.9c-0.2,0-0.3-0.1-0.4-0.2l-2.8-2.8c-0.2-0.2-0.2-0.6,0-0.8l2.8-2.8c0.2-0.2,0.6-0.2,0.9,0s0.2,0.6,0,0.9l-2.4,2.4
-      l2.4,2.4c0.2,0.2,0.2,0.6,0,0.9C15.4,14.8,15.3,14.9,15.1,14.9z"
-    />
-  </svg>
-);
-
-export default SidebarDrawerIcon;

+ 2 - 2
apps/app/src/components/InAppNotification/InAppNotificationDropdown.tsx

@@ -84,14 +84,14 @@ export const InAppNotificationDropdown = (): JSX.Element => {
   return (
     <Dropdown className="notification-wrapper grw-notification-dropdown" isOpen={isOpen} toggle={toggleDropdownHandler} direction="end">
       <DropdownToggle className="px-3" color="primary" innerRef={buttonRef}>
-        <i className="icon-bell" /> {badge}
+        <span className="material-symbols-outlined">notifications</span> {badge}
       </DropdownToggle>
       <DropdownMenu end>
         { inAppNotificationData != null && inAppNotificationData.docs.length === 0
           // no items
           ? <DropdownItem disabled>{t('in_app_notification.mark_all_as_read')}</DropdownItem>
           // render DropdownItem
-          : <InAppNotificationList type="dropdown-item" inAppNotificationData={inAppNotificationData} />
+          : <InAppNotificationList inAppNotificationData={inAppNotificationData} />
         }
         <DropdownItem divider />
         <DropdownItem tag="a" href="/me/all-in-app-notifications">

+ 21 - 49
apps/app/src/components/InAppNotification/InAppNotificationElm.tsx

@@ -1,43 +1,39 @@
-import React, {
-  FC, useRef,
-} from 'react';
+import React, { FC } from 'react';
 
-import type { IUser, IPage, HasObjectId } from '@growi/core';
+import type { HasObjectId } from '@growi/core';
 import { UserPicture } from '@growi/ui/dist/components';
-import { DropdownItem } from 'reactstrap';
 
-import { IInAppNotificationOpenable } from '~/client/interfaces/in-app-notification-openable';
 import { apiv3Post } from '~/client/util/apiv3-client';
-import { SupportedTargetModel } from '~/interfaces/activity';
 import { IInAppNotification, InAppNotificationStatuses } from '~/interfaces/in-app-notification';
 
-// Change the display for each targetmodel
-import PageModelNotification from './PageNotification/PageModelNotification';
-import UserModelNotification from './PageNotification/UserModelNotification';
+import { useModelNotification } from './PageNotification';
 
 interface Props {
   notification: IInAppNotification & HasObjectId
-  elemClassName?: string,
-  type?: 'button' | 'dropdown-item',
+  onUnopenedNotificationOpend?: () => void,
 }
 
-
 const InAppNotificationElm: FC<Props> = (props: Props) => {
 
-  const { notification } = props;
+  const { notification, onUnopenedNotificationOpend } = props;
+
+  const modelNotificationUtils = useModelNotification(notification);
 
-  const notificationRef = useRef<IInAppNotificationOpenable>(null);
+  const Notification = modelNotificationUtils?.Notification;
+  const publishOpen = modelNotificationUtils?.publishOpen;
+
+  if (Notification == null || publishOpen == null) {
+    return <></>;
+  }
 
   const clickHandler = async(notification: IInAppNotification & HasObjectId): Promise<void> => {
     if (notification.status === InAppNotificationStatuses.STATUS_UNOPENED) {
       // set notification status "OPEND"
       await apiv3Post('/in-app-notification/open', { id: notification._id });
+      onUnopenedNotificationOpend?.();
     }
 
-    const currentInstance = notificationRef.current;
-    if (currentInstance != null) {
-      currentInstance.open();
-    }
+    publishOpen();
   };
 
   const renderActionUserPictures = (): JSX.Element => {
@@ -59,24 +55,8 @@ const InAppNotificationElm: FC<Props> = (props: Props) => {
     );
   };
 
-  const isDropdownItem = props.type === 'dropdown-item';
-
-  const isPageNotification = (notification: IInAppNotification): notification is IInAppNotification<IPage> => {
-    return notification.targetModel === SupportedTargetModel.MODEL_PAGE;
-  };
-
-  const isUserNotification = (notification: IInAppNotification): notification is IInAppNotification<IUser> => {
-    return notification.targetModel === SupportedTargetModel.MODEL_USER;
-  };
-
-  // determine tag
-  const TagElem = isDropdownItem
-    ? DropdownItem
-    // eslint-disable-next-line react/prop-types
-    : props => <button type="button" {...props}>{props.children}</button>;
-
   return (
-    <TagElem className={props.elemClassName} onClick={() => clickHandler(notification)}>
+    <div className="list-group-item list-group-item-action" onClick={() => clickHandler(notification)} style={{ cursor: 'pointer' }}>
       <div className="d-flex align-items-center">
         <span
           className={`${notification.status === InAppNotificationStatuses.STATUS_UNOPENED
@@ -85,21 +65,13 @@ const InAppNotificationElm: FC<Props> = (props: Props) => {
           } rounded-circle me-3`}
         >
         </span>
+
         {renderActionUserPictures()}
-        {isPageNotification(notification) && (
-          <PageModelNotification
-            ref={notificationRef}
-            notification={notification}
-          />
-        )}
-        {isUserNotification(notification) && (
-          <UserModelNotification
-            ref={notificationRef}
-            notification={notification}
-          />
-        )}
+
+        <Notification />
+
       </div>
-    </TagElem>
+    </div>
   );
 };
 

+ 9 - 6
apps/app/src/components/InAppNotification/InAppNotificationList.tsx

@@ -9,12 +9,11 @@ import InAppNotificationElm from './InAppNotificationElm';
 
 type Props = {
   inAppNotificationData?: PaginateResult<IInAppNotification>,
-  elemClassName?: string,
-  type?: 'button' | 'dropdown-item',
+  onUnopenedNotificationOpend?: () => void,
 };
 
 const InAppNotificationList: FC<Props> = (props: Props) => {
-  const { inAppNotificationData } = props;
+  const { inAppNotificationData, onUnopenedNotificationOpend } = props;
 
   if (inAppNotificationData == null) {
     return (
@@ -29,13 +28,17 @@ const InAppNotificationList: FC<Props> = (props: Props) => {
   const notifications = inAppNotificationData.docs;
 
   return (
-    <>
+    <div className="list-group">
       { notifications.map((notification: IInAppNotification & HasObjectId) => {
         return (
-          <InAppNotificationElm key={notification._id} notification={notification} type={props.type} elemClassName={props.elemClassName} />
+          <InAppNotificationElm
+            key={notification._id}
+            notification={notification}
+            onUnopenedNotificationOpend={onUnopenedNotificationOpend}
+          />
         );
       }) }
-    </>
+    </div>
   );
 };
 

+ 1 - 3
apps/app/src/components/InAppNotification/InAppNotificationPage.tsx

@@ -100,9 +100,7 @@ export const InAppNotificationPage: FC = () => {
           ? t('in_app_notification.mark_all_as_read')
           // render list-group
           : (
-            <div className="list-group">
-              <InAppNotificationList inAppNotificationData={notificationData} type="button" elemClassName="list-group-item list-group-item-action" />
-            </div>
+            <InAppNotificationList inAppNotificationData={notificationData} />
           )
         }
 

+ 2 - 11
apps/app/src/components/InAppNotification/PageNotification/ModelNotification.tsx

@@ -1,9 +1,8 @@
-import React, { FC, useImperativeHandle } from 'react';
+import React, { FC } from 'react';
 
 import type { HasObjectId } from '@growi/core';
 import { PagePathLabel } from '@growi/ui/dist/components';
 
-import type { IInAppNotificationOpenable } from '~/client/interfaces/in-app-notification-openable';
 import type { IInAppNotification } from '~/interfaces/in-app-notification';
 
 import FormattedDistanceDate from '../../FormattedDistanceDate';
@@ -13,21 +12,13 @@ type Props = {
   actionMsg: string
   actionIcon: string
   actionUsers: string
-  publishOpen:() => void
-  ref: React.ForwardedRef<IInAppNotificationOpenable>
 };
 
 export const ModelNotification: FC<Props> = (props) => {
   const {
-    notification, actionMsg, actionIcon, actionUsers, publishOpen, ref,
+    notification, actionMsg, actionIcon, actionUsers,
   } = props;
 
-  useImperativeHandle(ref, () => ({
-    open() {
-      publishOpen();
-    },
-  }));
-
   return (
     <div className="p-2 overflow-hidden">
       <div className="text-truncate">

+ 34 - 25
apps/app/src/components/InAppNotification/PageNotification/PageModelNotification.tsx

@@ -1,29 +1,27 @@
 import React, {
-  forwardRef, ForwardRefRenderFunction, useCallback,
+  FC, useCallback,
 } from 'react';
 
 import type { IPage, HasObjectId } from '@growi/core';
 import { useRouter } from 'next/router';
 
-import type { IInAppNotificationOpenable } from '~/client/interfaces/in-app-notification-openable';
+import { SupportedTargetModel } from '~/interfaces/activity';
 import type { IInAppNotification } from '~/interfaces/in-app-notification';
 import * as pageSerializers from '~/models/serializers/in-app-notification-snapshot/page';
 
 import { ModelNotification } from './ModelNotification';
-import { useActionMsgAndIconForPageModelNotification } from './useActionAndMsg';
+import { useActionMsgAndIconForModelNotification } from './useActionAndMsg';
 
 
-interface Props {
-  notification: IInAppNotification<IPage> & HasObjectId
+export interface ModelNotificationUtils {
+  Notification: FC
+  publishOpen: () => void
 }
 
-const PageModelNotification: ForwardRefRenderFunction<IInAppNotificationOpenable, Props> = (props: Props, ref) => {
-
-  const { notification } = props;
-
-  const { actionMsg, actionIcon } = useActionMsgAndIconForPageModelNotification(notification);
+export const usePageModelNotification = (notification: IInAppNotification & HasObjectId): ModelNotificationUtils | null => {
 
   const router = useRouter();
+  const { actionMsg, actionIcon } = useActionMsgAndIconForModelNotification(notification);
 
   const getActionUsers = useCallback(() => {
     const latestActionUsers = notification.actionUsers.slice(0, 3);
@@ -46,31 +44,42 @@ const PageModelNotification: ForwardRefRenderFunction<IInAppNotificationOpenable
     return actionedUsers;
   }, [notification.actionUsers]);
 
+  const isPageModelNotification = (notification: IInAppNotification & HasObjectId): notification is IInAppNotification<IPage> & HasObjectId => {
+    return notification.targetModel === SupportedTargetModel.MODEL_PAGE;
+  };
+
+  if (!isPageModelNotification(notification)) {
+    return null;
+  }
+
   const actionUsers = getActionUsers();
 
-  // publish open()
+  notification.parsedSnapshot = pageSerializers.parseSnapshot(notification.snapshot);
+
+  const Notification = () => {
+    return (
+      <ModelNotification
+        notification={notification}
+        actionMsg={actionMsg}
+        actionIcon={actionIcon}
+        actionUsers={actionUsers}
+      />
+    );
+  };
+
   const publishOpen = () => {
     if (notification.target != null) {
       // jump to target page
-      const targetPagePath = notification.target.path;
+      const targetPagePath = (notification.target as IPage).path;
       if (targetPagePath != null) {
         router.push(targetPagePath);
       }
     }
   };
 
-  notification.parsedSnapshot = pageSerializers.parseSnapshot(notification.snapshot);
+  return {
+    Notification,
+    publishOpen,
+  };
 
-  return (
-    <ModelNotification
-      notification={notification}
-      actionMsg={actionMsg}
-      actionIcon={actionIcon}
-      actionUsers={actionUsers}
-      publishOpen={publishOpen}
-      ref={ref}
-    />
-  );
 };
-
-export default forwardRef(PageModelNotification);

+ 30 - 26
apps/app/src/components/InAppNotification/PageNotification/UserModelNotification.tsx

@@ -1,45 +1,49 @@
-import React, {
-  forwardRef, ForwardRefRenderFunction,
-} from 'react';
+import React from 'react';
 
 import type { IUser, HasObjectId } from '@growi/core';
 import { useRouter } from 'next/router';
 
-import type { IInAppNotificationOpenable } from '~/client/interfaces/in-app-notification-openable';
+import { SupportedTargetModel } from '~/interfaces/activity';
 import type { IInAppNotification } from '~/interfaces/in-app-notification';
 
 import { ModelNotification } from './ModelNotification';
-import { useActionMsgAndIconForUserModelNotification } from './useActionAndMsg';
+import { ModelNotificationUtils } from './PageModelNotification';
+import { useActionMsgAndIconForModelNotification } from './useActionAndMsg';
 
-interface Props {
-  notification: IInAppNotification<IUser> & HasObjectId
-}
 
-const UserModelNotification: ForwardRefRenderFunction<IInAppNotificationOpenable, Props> = (props: Props, ref) => {
+export const useUserModelNotification = (notification: IInAppNotification & HasObjectId): ModelNotificationUtils | null => {
 
-  const { notification } = props;
+  const { actionMsg, actionIcon } = useActionMsgAndIconForModelNotification(notification);
+  const router = useRouter();
 
-  const { actionMsg, actionIcon } = useActionMsgAndIconForUserModelNotification(notification);
+  const isUserModelNotification = (notification: IInAppNotification & HasObjectId): notification is IInAppNotification<IUser> & HasObjectId => {
+    return notification.targetModel === SupportedTargetModel.MODEL_USER;
+  };
 
-  const router = useRouter();
+  if (!isUserModelNotification(notification)) {
+    return null;
+  }
+
+  const actionUsers = notification.target.username;
+
+  const Notification = () => {
+    return (
+      <ModelNotification
+        notification={notification}
+        actionMsg={actionMsg}
+        actionIcon={actionIcon}
+        actionUsers={actionUsers}
+      />
+    );
+  };
 
-  // publish open()
   const publishOpen = () => {
     router.push('/admin/users');
   };
 
-  const actionUsers = notification.target.username;
+  return {
+    Notification,
+    publishOpen,
+  };
 
-  return (
-    <ModelNotification
-      notification={notification}
-      actionMsg={actionMsg}
-      actionIcon={actionIcon}
-      actionUsers={actionUsers}
-      publishOpen={publishOpen}
-      ref={ref}
-    />
-  );
 };
-
-export default forwardRef(UserModelNotification);

+ 19 - 0
apps/app/src/components/InAppNotification/PageNotification/index.tsx

@@ -0,0 +1,19 @@
+import type { HasObjectId } from '@growi/core';
+
+import type { IInAppNotification } from '~/interfaces/in-app-notification';
+
+
+import { usePageModelNotification, type ModelNotificationUtils } from './PageModelNotification';
+import { useUserModelNotification } from './UserModelNotification';
+
+
+export const useModelNotification = (notification: IInAppNotification & HasObjectId): ModelNotificationUtils | null => {
+
+  const pageModelNotificationUtils = usePageModelNotification(notification);
+  const userModelNotificationUtils = useUserModelNotification(notification);
+
+  const modelNotificationUtils = pageModelNotificationUtils ?? userModelNotificationUtils;
+
+
+  return modelNotificationUtils;
+};

+ 2 - 19
apps/app/src/components/InAppNotification/PageNotification/useActionAndMsg.ts

@@ -1,4 +1,4 @@
-import type { IUser, IPage, HasObjectId } from '@growi/core';
+import type { HasObjectId } from '@growi/core';
 
 import { SupportedAction } from '~/interfaces/activity';
 import type { IInAppNotification } from '~/interfaces/in-app-notification';
@@ -8,7 +8,7 @@ export type ActionMsgAndIconType = {
   actionIcon: string
 }
 
-export const useActionMsgAndIconForPageModelNotification = (notification: IInAppNotification<IPage> & HasObjectId): ActionMsgAndIconType => {
+export const useActionMsgAndIconForModelNotification = (notification: IInAppNotification & HasObjectId): ActionMsgAndIconType => {
   const actionType: string = notification.action;
   let actionMsg: string;
   let actionIcon: string;
@@ -66,23 +66,6 @@ export const useActionMsgAndIconForPageModelNotification = (notification: IInApp
       actionMsg = 'commented on';
       actionIcon = 'icon-bubble';
       break;
-    default:
-      actionMsg = '';
-      actionIcon = '';
-  }
-
-  return {
-    actionMsg,
-    actionIcon,
-  };
-};
-
-export const useActionMsgAndIconForUserModelNotification = (notification: IInAppNotification<IUser> & HasObjectId): ActionMsgAndIconType => {
-  const actionType: string = notification.action;
-  let actionMsg: string;
-  let actionIcon: string;
-
-  switch (actionType) {
     case SupportedAction.ACTION_USER_REGISTRATION_APPROVAL_REQUEST:
       actionMsg = 'requested registration approval';
       actionIcon = 'icon-bubble';

+ 6 - 6
apps/app/src/components/InstallerForm.tsx

@@ -84,7 +84,7 @@ const InstallerForm = memo((): JSX.Element => {
   const hasErrorClass = isValidUserName ? '' : ' has-error';
   const unavailableUserId = isValidUserName
     ? ''
-    : <span><i className="icon-fw icon-ban" />{ t('installer.unavaliable_user_id') }</span>;
+    : <span><span className="material-symbols-outlined">block</span>{ t('installer.unavaliable_user_id') }</span>;
 
   return (
     <div data-testid="installerForm" className={`nologin-dialog p-3 mx-auto${hasErrorClass}`}>
@@ -100,7 +100,7 @@ const InstallerForm = memo((): JSX.Element => {
         <form role="form" id="register-form" className="col-md-12" onSubmit={submitHandler}>
           <div className="dropdown mb-3">
             <div className="input-group dropdown-with-icon">
-              <span className="input-group-text"><i className="icon-bubbles" /></span>
+              <span className="input-group-text"></span><span className="material-symbols-outlined">language</span>
               <button
                 type="button"
                 className="btn btn-secondary dropdown-toggle form-control text-end rounded-end"
@@ -145,7 +145,7 @@ const InstallerForm = memo((): JSX.Element => {
           </div>
 
           <div className={`input-group mb-3${hasErrorClass}`}>
-            <span className="input-group-text"><i className="icon-user" /></span>
+            <span className="input-group-text"></span><span className="material-symbols-outlined">person</span>
             <input
               data-testid="tiUsername"
               type="text"
@@ -159,7 +159,7 @@ const InstallerForm = memo((): JSX.Element => {
           <p className="form-text">{ unavailableUserId }</p>
 
           <div className="input-group mb-3">
-            <span className="input-group-text"><i className="icon-tag" /></span>
+            <span className="input-group-text"></span><span className="material-symbols-outlined">sell</span>
             <input
               data-testid="tiName"
               type="text"
@@ -171,7 +171,7 @@ const InstallerForm = memo((): JSX.Element => {
           </div>
 
           <div className="input-group mb-3">
-            <span className="input-group-text"><i className="icon-envelope" /></span>
+            <span className="input-group-text"></span><span className="material-symbols-outlined">mail</span>
             <input
               data-testid="tiEmail"
               type="email"
@@ -183,7 +183,7 @@ const InstallerForm = memo((): JSX.Element => {
           </div>
 
           <div className="input-group mb-3">
-            <span className="input-group-text"><i className="icon-lock" /></span>
+            <span className="input-group-text"></span> <span className="material-symbols-outlined">lock</span>
             <input
               data-testid="tiPassword"
               type="password"

+ 4 - 4
apps/app/src/components/InvitedForm.tsx

@@ -83,7 +83,7 @@ export const InvitedForm = (props: InvitedFormProps): JSX.Element => {
         {/* Email Form */}
         <div className="input-group">
           <span className="input-group-text">
-            <i className="icon-envelope"></i>
+            <span className="material-symbols-outlined">mail</span>
           </span>
           <input
             type="text"
@@ -98,7 +98,7 @@ export const InvitedForm = (props: InvitedFormProps): JSX.Element => {
         {/* UserID Form */}
         <div className="input-group" id="input-group-username">
           <span className="input-group-text">
-            <i className="icon-user"></i>
+            <span className="material-symbols-outlined">person</span>
           </span>
           <input
             type="text"
@@ -112,7 +112,7 @@ export const InvitedForm = (props: InvitedFormProps): JSX.Element => {
         {/* Name Form */}
         <div className="input-group">
           <span className="input-group-text">
-            <i className="icon-tag"></i>
+            <span className="material-symbols-outlined">sell</span>
           </span>
           <input
             type="text"
@@ -126,7 +126,7 @@ export const InvitedForm = (props: InvitedFormProps): JSX.Element => {
         {/* Password Form */}
         <div className="input-group">
           <span className="input-group-text">
-            <i className="icon-lock"></i>
+            <span className="material-symbols-outlined">lock</span>
           </span>
           <input
             type="password"

+ 1 - 3
apps/app/src/components/ItemsTree/ItemsTree.module.scss

@@ -65,9 +65,7 @@ $grw-pagetree-item-container-height: 40px;
 
     .grw-pagetree-item-container {
       .grw-triangle-container {
-        // TODO: ignore width frickering
-        // https://redmine.weseek.co.jp/issues/130828
-        // min-width: 35px;
+        min-width: 35px;
         height: $grw-pagetree-item-container-height;
       }
     }

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

@@ -25,7 +25,7 @@ import { mutateSearching } from '~/stores/search';
 import { usePageTreeDescCountMap, useSidebarScrollerRef } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 
-import { ItemNode, SimpleItemProps } from '../TreeItem';
+import { ItemNode, type TreeItemProps } from '../TreeItem';
 
 import ItemsTreeContentSkeleton from './ItemsTreeContentSkeleton';
 
@@ -92,7 +92,7 @@ type ItemsTreeProps = {
   targetPath: string
   targetPathOrId?: Nullable<string>
   targetAndAncestorsData?: TargetAndAncestors
-  CustomTreeItem: React.FunctionComponent<SimpleItemProps>
+  CustomTreeItem: React.FunctionComponent<TreeItemProps>
 }
 
 /*

+ 2 - 0
apps/app/src/components/Layout/BasicLayout.tsx

@@ -23,6 +23,7 @@ const PageRenameModal = dynamic(() => import('../PageRenameModal'), { ssr: false
 const PagePresentationModal = dynamic(() => import('../PagePresentationModal'), { ssr: false });
 const PageAccessoriesModal = dynamic(() => import('../PageAccessoriesModal').then(mod => mod.PageAccessoriesModal), { ssr: false });
 const DeleteBookmarkFolderModal = dynamic(() => import('../DeleteBookmarkFolderModal').then(mod => mod.DeleteBookmarkFolderModal), { ssr: false });
+const SearchModal = dynamic(() => import('../../features/search/client/components/SearchModal'), { ssr: false });
 
 
 type Props = {
@@ -57,6 +58,7 @@ export const BasicLayout = ({ children, className }: Props): JSX.Element => {
         <DeleteAttachmentModal />
         <DeleteBookmarkFolderModal />
         <PutbackPageModal />
+        <SearchModal />
       </DndProvider>
 
       <PagePresentationModal />

+ 16 - 14
apps/app/src/components/LoginForm.tsx

@@ -181,12 +181,13 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
         {/* !! - DO NOT DELETE HIDDEN ELEMENT - !! -- 7.12 ryoji-s */}
         {/* Import font-awesome to prevent MongoStore.js "Unable to find the session to touch" error */}
         <div className="visually-hidden">
+          {/* Unsettled 11.17 meiri-k */}
           <i className="fa fa-spinner fa-pulse" />
         </div>
         {/* !! - END OF HIDDEN ELEMENT - !! */}
         {isLdapSetupFailed && (
           <div className="alert alert-warning small">
-            <strong><i className="icon-fw icon-info"></i>{t('login.enabled_ldap_has_configuration_problem')}</strong><br />
+            <strong><span className="material-symbols-outlined">info</span>{t('login.enabled_ldap_has_configuration_problem')}</strong><br />
             <span dangerouslySetInnerHTML={{ __html: t('login.set_env_var_for_logs') }}></span>
           </div>
         )}
@@ -196,7 +197,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
         <form role="form" onSubmit={handleLoginWithLocalSubmit} id="login-form">
           <div className="input-group">
             <span className="input-group-text">
-              <i className="icon-user"></i>
+              <span className="material-symbols-outlined">person</span>
             </span>
             <input
               type="text"
@@ -208,14 +209,14 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
             />
             {isLdapStrategySetup && (
               <small className="input-group-text text-success">
-                <i className="icon-fw icon-check"></i> LDAP
+                <span className="material-symbols-outlined">select_check_box</span>LDAP
               </small>
             )}
           </div>
 
           <div className="input-group">
             <span className="input-group-text">
-              <i className="icon-lock"></i>
+              <span className="material-symbols-outlined">lock</span>
             </span>
             <input
               type="password"
@@ -237,7 +238,8 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
             >
               <div className="eff"></div>
               <span className="btn-label">
-                <i className={isLoading ? 'fa fa-spinner fa-pulse me-1' : 'icon-login'} />
+                {/* spinner.Tentative decision meiri-k 11.17 */}
+                <span className="material-symbols-outlined">{isLoading ? 'hoge' : 'login'}</span>
               </span>
               <span className="btn-label-text">{t('Sign in')}</span>
             </button>
@@ -416,7 +418,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
             <div>
               <div className="input-group" id="input-group-username">
                 <span className="input-group-text">
-                  <i className="icon-user"></i>
+                  <span className="material-symbols-outlined">person</span>
                 </span>
                 {/* username */}
                 <input
@@ -434,7 +436,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
               </p>
               <div className="input-group">
                 <span className="input-group-text">
-                  <i className="icon-tag"></i>
+                  <span className="material-symbols-outlined">sell</span>
                 </span>
                 {/* name */}
                 <input
@@ -452,7 +454,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
 
           <div className="input-group">
             <span className="input-group-text">
-              <i className="icon-envelope"></i>
+              <span className="material-symbols-outlined">mail</span>
             </span>
             {/* email */}
             <input
@@ -486,7 +488,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
             <div>
               <div className="input-group">
                 <span className="input-group-text">
-                  <i className="icon-lock"></i>
+                  <span className="material-symbols-outlined">lock</span>
                 </span>
                 {/* Password */}
                 <input
@@ -511,7 +513,8 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
             >
               <div className="eff"></div>
               <span className="btn-label">
-                <i className={isLoading ? 'fa fa-spinner fa-pulse me-1' : 'icon-user-follow'} />
+                {/* spinner.Tentative decision meiri-k 11.17 */}
+                <span className="material-symbols-outlined">{isLoading ? 'hoge' : 'login'}</span>
               </span>
               <span className="btn-label-text">{submitText}</span>
             </button>
@@ -529,8 +532,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
               style={{ pointerEvents: isLoading ? 'none' : 'auto' }}
               onClick={switchForm}
             >
-              <i className="icon-fw icon-login"></i>
-              {t('Sign in is here')}
+              <span className="material-symbols-outlined">login</span>{t('Sign in is here')}
             </a>
           </div>
         </div>
@@ -557,7 +559,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
                 {isLocalOrLdapStrategiesEnabled && isPasswordResetEnabled && (
                   <div className="text-end mb-2">
                     <a href="/forgot-password" className="d-block link-switch">
-                      <i className="icon-key"></i> {t('forgot_password.forgot_password')}
+                      <span className="material-symbols-outlined">vpn_key</span>{t('forgot_password.forgot_password')}
                     </a>
                   </div>
                 )}
@@ -571,7 +573,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
                       style={{ pointerEvents: isLoading ? 'none' : 'auto' }}
                       onClick={switchForm}
                     >
-                      <i className="ti ti-check-box"></i> {t('Sign up is here')}
+                      <span className="material-symbols-outlined">check_box</span> {t('Sign up is here')}
                     </a>
                   </div>
                 )}

+ 0 - 0
apps/app/src/components/Me/ColorModeSettings.module.scss


+ 70 - 0
apps/app/src/components/Me/ColorModeSettings.tsx

@@ -0,0 +1,70 @@
+import React, { useCallback } from 'react';
+
+import { useTranslation } from 'react-i18next';
+
+import { Themes, useNextThemes } from '~/stores/use-next-themes';
+
+// import styles from './ColorModeSettings.module.scss';
+
+
+type ColorModeSettingsButtonProps = {
+  isActive: boolean,
+  children?: React.ReactNode,
+  onClick?: () => void,
+}
+
+const ColorModeSettingsButton = ({ isActive, children, onClick }: ColorModeSettingsButtonProps): JSX.Element => {
+  return (
+    <button
+      type="button"
+      onClick={onClick}
+      className={`btn py-2 px-4 fw-bold border-3 ${isActive ? 'btn-outline-primary' : 'btn-outline-neutral-secondary'}`}
+    >
+      { children }
+    </button>
+  );
+};
+
+
+export const ColorModeSettings = (): JSX.Element => {
+  const { t } = useTranslation();
+
+  const { setTheme, theme } = useNextThemes();
+
+  const isActive = useCallback((targetTheme: Themes) => {
+    return targetTheme === theme;
+  }, [theme]);
+
+  return (
+    <div>
+      <h2 className="border-bottom mb-4">{t('color_mode_settings.settings')}</h2>
+
+      <div className="offset-md-3">
+
+        <div className="d-flex column-gap-3">
+
+          <ColorModeSettingsButton isActive={isActive(Themes.LIGHT)} onClick={() => { setTheme(Themes.LIGHT) }}>
+            <span className="material-symbols-outlined fs-5 me-1">light_mode</span>
+            <span>{t('color_mode_settings.light')}</span>
+          </ColorModeSettingsButton>
+
+          <ColorModeSettingsButton isActive={isActive(Themes.DARK)} onClick={() => { setTheme(Themes.DARK) }}>
+            <span className="material-symbols-outlined fs-5 me-1">dark_mode</span>
+            <span>{t('color_mode_settings.dark')}</span>
+          </ColorModeSettingsButton>
+
+          <ColorModeSettingsButton isActive={isActive(Themes.SYSTEM)} onClick={() => { setTheme(Themes.SYSTEM) }}>
+            <span className="material-symbols-outlined fs-5 me-1">devices</span>
+            <span>{t('color_mode_settings.system')}</span>
+          </ColorModeSettingsButton>
+
+        </div>
+
+        <div className="mt-3 text-muted">
+          {/* eslint-disable-next-line react/no-danger */}
+          <span dangerouslySetInnerHTML={{ __html: t('color_mode_settings.description') }} />
+        </div>
+      </div>
+    </div>
+  );
+};

+ 10 - 94
apps/app/src/components/Me/OtherSettings.tsx

@@ -1,105 +1,21 @@
-import {
-  useState, useEffect, useCallback,
-} from 'react';
-
-import { useTranslation } from 'next-i18next';
-import { UncontrolledTooltip } from 'reactstrap';
-
-import { apiv3Put } from '~/client/util/apiv3-client';
-import { toastSuccess, toastError } from '~/client/util/toastr';
-import { useSWRxIsQuestionnaireEnabled } from '~/features/questionnaire/client/stores/questionnaire';
-import { useCurrentUser } from '~/stores/context';
+import { ColorModeSettings } from './ColorModeSettings';
+import { QuestionnaireSettings } from './QuestionnaireSettings';
+import { UISettings } from './UISettings';
 
 const OtherSettings = (): JSX.Element => {
-  const { t } = useTranslation();
-  const { data: currentUser, error: errorCurrentUser } = useCurrentUser();
-  const { data: growiIsQuestionnaireEnabled } = useSWRxIsQuestionnaireEnabled();
-
-  const [isQuestionnaireEnabled, setIsQuestionnaireEnabled] = useState(currentUser?.isQuestionnaireEnabled);
-
-  const onChangeIsQuestionnaireEnabledHandler = useCallback(async() => {
-    setIsQuestionnaireEnabled(prev => !prev);
-  }, []);
-
-  const onClickUpdateIsQuestionnaireEnabledHandler = useCallback(async() => {
-    try {
-      await apiv3Put('/personal-setting/questionnaire-settings', {
-        isQuestionnaireEnabled,
-      });
-      toastSuccess(t('toaster.update_successed', { target: t('questionnaire.settings'), ns: 'commons' }));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }, [isQuestionnaireEnabled, t]);
-
-  // Sync currentUser and state
-  useEffect(() => {
-    setIsQuestionnaireEnabled(currentUser?.isQuestionnaireEnabled);
-  }, [currentUser?.isQuestionnaireEnabled]);
-
-  const isLoadingCurrentUser = currentUser === undefined && errorCurrentUser === undefined;
 
   return (
     <>
-      <h2 className="border-bottom my-4">{t('questionnaire.settings')}</h2>
-
-      {isLoadingCurrentUser && (
-        <div className="text-muted text-center mb-5">
-          <i className="fa fa-2x fa-spinner fa-pulse me-1" />
-        </div>
-      )}
+      <div className="mt-4">
+        <ColorModeSettings />
+      </div>
 
-      <div className="row">
-        <div className="offset-md-3 col-md-6 text-start">
-          {!isLoadingCurrentUser && (
-            <div className="form-check form-switch form-check-primary">
-              <span id="grw-questionnaire-settings-toggle-wrapper">
-                <input
-                  type="checkbox"
-                  className="form-check-input"
-                  id="isQuestionnaireEnabled"
-                  checked={growiIsQuestionnaireEnabled && isQuestionnaireEnabled}
-                  onChange={onChangeIsQuestionnaireEnabledHandler}
-                  disabled={!growiIsQuestionnaireEnabled}
-                />
-                <label className="form-label form-check-label" htmlFor="isQuestionnaireEnabled">
-                  {t('questionnaire.enable_questionnaire')}
-                </label>
-              </span>
-              <p className="form-text text-muted small">
-                {t('questionnaire.personal_settings_explanation')}
-              </p>
-              {!growiIsQuestionnaireEnabled && (
-                <UncontrolledTooltip placement="bottom" target="grw-questionnaire-settings-toggle-wrapper">
-                  {t('questionnaire.disabled_by_admin')}
-                </UncontrolledTooltip>
-              ) }
-            </div>
-          )}
-        </div>
+      <div className="mt-4">
+        <UISettings />
       </div>
 
-      <div className="row my-3">
-        <div className="offset-4 col-5">
-          <span className="d-inline-block" id="grw-questionnaire-settings-update-btn-wrapper">
-            <button
-              data-testid="grw-questionnaire-settings-update-btn"
-              type="button"
-              className="btn btn-primary"
-              onClick={onClickUpdateIsQuestionnaireEnabledHandler}
-              disabled={!growiIsQuestionnaireEnabled}
-              style={growiIsQuestionnaireEnabled ? {} : { pointerEvents: 'none' }}
-            >
-              {t('Update')}
-            </button>
-          </span>
-          {!growiIsQuestionnaireEnabled && (
-            <UncontrolledTooltip placement="bottom" target="grw-questionnaire-settings-update-btn-wrapper">
-              {t('questionnaire.disabled_by_admin')}
-            </UncontrolledTooltip>
-          )}
-        </div>
+      <div className="mt-4">
+        <QuestionnaireSettings />
       </div>
     </>
   );

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

@@ -40,7 +40,7 @@ const PersonalSettings = () => {
         i18n: t('API Settings'),
       },
       // editor_settings: {
-      //   Icon: () => <i className="icon-fw icon-pencil"></i>,
+      //   Icon: () => <span className="material-symbols-outlined">edit</span>,
       //   Content: EditorSettings,
       //   i18n: t('editor_settings.editor_settings'),
       // },

+ 106 - 0
apps/app/src/components/Me/QuestionnaireSettings.tsx

@@ -0,0 +1,106 @@
+import { useCallback, useEffect, useState } from 'react';
+
+import { useTranslation } from 'react-i18next';
+import { UncontrolledTooltip } from 'reactstrap';
+
+import { apiv3Put } from '~/client/util/apiv3-client';
+import { toastError, toastSuccess } from '~/client/util/toastr';
+import { useSWRxIsQuestionnaireEnabled } from '~/features/questionnaire/client/stores/questionnaire';
+import { useCurrentUser } from '~/stores/context';
+
+
+export const QuestionnaireSettings = (): JSX.Element => {
+  const { t } = useTranslation();
+  const { data: currentUser, error: errorCurrentUser } = useCurrentUser();
+  const { data: growiIsQuestionnaireEnabled } = useSWRxIsQuestionnaireEnabled();
+
+  const [isQuestionnaireEnabled, setIsQuestionnaireEnabled] = useState(currentUser?.isQuestionnaireEnabled);
+
+  const onChangeIsQuestionnaireEnabledHandler = useCallback(async() => {
+    setIsQuestionnaireEnabled(prev => !prev);
+  }, []);
+
+  const onClickUpdateIsQuestionnaireEnabledHandler = useCallback(async() => {
+    try {
+      await apiv3Put('/personal-setting/questionnaire-settings', {
+        isQuestionnaireEnabled,
+      });
+      toastSuccess(t('toaster.update_successed', { target: t('questionnaire.settings'), ns: 'commons' }));
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }, [isQuestionnaireEnabled, t]);
+
+  // Sync currentUser and state
+  useEffect(() => {
+    setIsQuestionnaireEnabled(currentUser?.isQuestionnaireEnabled);
+  }, [currentUser?.isQuestionnaireEnabled]);
+
+  const isLoadingCurrentUser = currentUser === undefined && errorCurrentUser === undefined;
+
+  return (
+    <>
+      <h2 className="border-bottom mb-4">{t('questionnaire.settings')}</h2>
+
+      {isLoadingCurrentUser && (
+        <div className="text-muted text-center mb-5">
+          <i className="fa fa-2x fa-spinner fa-pulse me-1" />
+        </div>
+      )}
+
+      <div className="row">
+        <div className="offset-md-3 col-md-6 text-start">
+          {!isLoadingCurrentUser && (
+            <div className="form-check form-switch">
+              <span id="grw-questionnaire-settings-toggle-wrapper">
+                <input
+                  type="checkbox"
+                  className="form-check-input"
+                  id="isQuestionnaireEnabled"
+                  checked={growiIsQuestionnaireEnabled && isQuestionnaireEnabled}
+                  onChange={onChangeIsQuestionnaireEnabledHandler}
+                  disabled={!growiIsQuestionnaireEnabled}
+                />
+                <label className="form-label form-check-label" htmlFor="isQuestionnaireEnabled">
+                  {t('questionnaire.enable_questionnaire')}
+                </label>
+              </span>
+              <p className="form-text text-muted small">
+                {t('questionnaire.personal_settings_explanation')}
+              </p>
+              {!growiIsQuestionnaireEnabled && (
+                <UncontrolledTooltip placement="bottom" target="grw-questionnaire-settings-toggle-wrapper">
+                  {t('questionnaire.disabled_by_admin')}
+                </UncontrolledTooltip>
+              ) }
+            </div>
+          )}
+        </div>
+      </div>
+
+      <div className="row my-3">
+        <div className="offset-4 col-5">
+          <span className="d-inline-block" id="grw-questionnaire-settings-update-btn-wrapper">
+            <button
+              data-testid="grw-questionnaire-settings-update-btn"
+              type="button"
+              className="btn btn-primary"
+              onClick={onClickUpdateIsQuestionnaireEnabledHandler}
+              disabled={!growiIsQuestionnaireEnabled}
+              style={growiIsQuestionnaireEnabled ? {} : { pointerEvents: 'none' }}
+            >
+              {t('Update')}
+            </button>
+          </span>
+          {!growiIsQuestionnaireEnabled && (
+            <UncontrolledTooltip placement="bottom" target="grw-questionnaire-settings-update-btn-wrapper">
+              {t('questionnaire.disabled_by_admin')}
+            </UncontrolledTooltip>
+          )}
+        </div>
+      </div>
+    </>
+
+  );
+};

+ 9 - 0
apps/app/src/components/Me/UISettings.module.scss

@@ -0,0 +1,9 @@
+@use '@growi/core/scss/bootstrap/init' as bs;
+
+.grw-sidebar-mode-icon {
+  display: flex;
+  align-items: center;
+  width: 20px;
+  height: 20px;
+  color: bs.$secondary;
+}

+ 111 - 0
apps/app/src/components/Me/UISettings.tsx

@@ -0,0 +1,111 @@
+import { useCallback } from 'react';
+
+import { useTranslation } from 'react-i18next';
+import { UncontrolledTooltip } from 'reactstrap';
+
+import { updateUserUISettings } from '~/client/services/user-ui-settings';
+import { toastError, toastSuccess } from '~/client/util/toastr';
+import { useCollapsedContentsOpened, usePreferCollapsedMode, useSidebarMode } from '~/stores/ui';
+
+import styles from './UISettings.module.scss';
+
+const IconWithTooltip = ({
+  id, label, children, additionalClasses,
+}: {
+id: string,
+label: string,
+children: JSX.Element,
+additionalClasses: string
+}) => (
+  <>
+    <div id={id} className={`${additionalClasses != null ? additionalClasses : ''}`}>{children}</div>
+    <UncontrolledTooltip placement="bottom" fade={false} target={id}>{label}</UncontrolledTooltip>
+  </>
+);
+
+export const UISettings = (): JSX.Element => {
+  const { t } = useTranslation();
+  const {
+    isDockMode, isCollapsedMode,
+  } = useSidebarMode();
+  const { mutate: mutatePreferCollapsedMode } = usePreferCollapsedMode();
+  const { mutate: mutateCollapsedContentsOpened } = useCollapsedContentsOpened();
+
+  const toggleCollapsed = useCallback(() => {
+    mutatePreferCollapsedMode(!isCollapsedMode());
+    mutateCollapsedContentsOpened(false);
+  }, [mutatePreferCollapsedMode, isCollapsedMode, mutateCollapsedContentsOpened]);
+
+  const updateButtonHandler = useCallback(async() => {
+    try {
+      await updateUserUISettings({ preferCollapsedModeByUser: isCollapsedMode() });
+      toastSuccess(t('toaster.update_successed', { target: t('ui_settings.side_bar_mode.settings'), ns: 'commons' }));
+    }
+    catch (err) {
+      toastError(err);
+    }
+
+  }, [isCollapsedMode, t]);
+
+  const renderSidebarModeSwitch = () => {
+    return (
+      <>
+        <div className="d-flex align-items-start">
+          <div className="d-flex align-items-center">
+            <IconWithTooltip
+              id="iwt-sidebar-collapsed"
+              label="Collapsed"
+              additionalClasses={styles['grw-sidebar-mode-icon']}
+            >
+              <span className="growi-custom-icons">sidebar-collapsed</span>
+            </IconWithTooltip>
+            <div className="form-check form-switch ms-1">
+
+              <input
+                id="swSidebarMode"
+                className="form-check-input"
+                type="checkbox"
+                checked={isDockMode()}
+                onChange={toggleCollapsed}
+              />
+              <label className="form-label form-check-label" htmlFor="swSidebarMode"></label>
+            </div>
+            <IconWithTooltip id="iwt-sidebar-dock" label="Dock" additionalClasses={styles['grw-sidebar-mode-icon']}>
+              <span className="growi-custom-icons">sidebar-dock</span>
+            </IconWithTooltip>
+          </div>
+          <div className="ms-2">
+            <label className="form-label form-check-label" htmlFor="swSidebarMode">
+              {t('ui_settings.side_bar_mode.side_bar_mode_setting')}
+            </label>
+            <p className="form-text text-muted small">{t('ui_settings.side_bar_mode.description')}</p>
+          </div>
+        </div>
+      </>
+    );
+  };
+
+  return (
+    <>
+      <h2 className="border-bottom mb-4">{t('ui_settings.ui_settings')}</h2>
+
+      <div className="row justify-content-center">
+        <div className="col-md-6">
+
+          { renderSidebarModeSwitch() }
+
+          <div>
+          </div>
+        </div>
+      </div>
+
+      <div className="row my-3">
+        <div className="offset-4 col-5">
+          <button data-testid="" type="button" className="btn btn-primary" onClick={updateButtonHandler}>
+            {t('Update')}
+          </button>
+        </div>
+      </div>
+    </>
+  );
+};

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