Przeglądaj źródła

Merge branch 'master' into feat/130798-do-not-insert-initial-value-when-input-is-empty-in-editor

Shun Miyazawa 1 rok temu
rodzic
commit
03e66577ca
100 zmienionych plików z 767 dodań i 479 usunięć
  1. 44 1
      CHANGELOG.md
  2. 16 0
      apps/app/config/i18next.config.js
  3. 20 9
      apps/app/config/next-i18next.config.js
  4. 1 1
      apps/app/docker/README.md
  5. 6 5
      apps/app/package.json
  6. 3 0
      apps/app/public/static/locales/en_US/translation.json
  7. 3 0
      apps/app/public/static/locales/ja_JP/translation.json
  8. 3 0
      apps/app/public/static/locales/zh_CN/translation.json
  9. 0 28
      apps/app/src/client/services/side-effects/yjs-draft.ts
  10. 33 0
      apps/app/src/client/services/side-effects/yjs.ts
  11. 3 3
      apps/app/src/client/util/locale-utils.ts
  12. 4 3
      apps/app/src/components/Admin/App/ConfirmModal.tsx
  13. 17 0
      apps/app/src/components/Admin/Common/AdminNavigation.module.scss
  14. 8 3
      apps/app/src/components/Admin/Common/AdminNavigation.tsx
  15. 17 8
      apps/app/src/components/Admin/Customize/CustomizeLayoutSetting.tsx
  16. 21 12
      apps/app/src/components/Admin/Customize/CustomizeSidebarSetting.tsx
  17. 7 4
      apps/app/src/components/Admin/Customize/CustomizeThemeOptions.tsx
  18. 6 0
      apps/app/src/components/Admin/Customize/ThemeColorBox.module.scss
  19. 15 5
      apps/app/src/components/Admin/Customize/ThemeColorBox.tsx
  20. 1 1
      apps/app/src/components/Admin/ExportArchiveData/SelectCollectionsModal.tsx
  21. 1 1
      apps/app/src/components/Admin/ImportData/GrowiArchive/ErrorViewer.tsx
  22. 1 1
      apps/app/src/components/Admin/ImportData/GrowiArchive/ImportCollectionConfigurationModal.jsx
  23. 1 1
      apps/app/src/components/Admin/Notification/NotificationDeleteModal.jsx
  24. 1 1
      apps/app/src/components/Admin/Security/DeleteAllShareLinksModal.jsx
  25. 1 1
      apps/app/src/components/Admin/Security/LdapAuthTestModal.jsx
  26. 1 1
      apps/app/src/components/Admin/SlackIntegration/ConfirmBotChangeModal.jsx
  27. 1 1
      apps/app/src/components/Admin/SlackIntegration/DeleteSlackBotSettingsModal.tsx
  28. 1 1
      apps/app/src/components/Admin/UserGroup/UserGroupDeleteModal.tsx
  29. 1 1
      apps/app/src/components/Admin/UserGroup/UserGroupModal.tsx
  30. 1 1
      apps/app/src/components/Admin/UserGroupDetail/UpdateParentConfirmModal.tsx
  31. 1 1
      apps/app/src/components/Admin/UserGroupDetail/UserGroupUserModal.tsx
  32. 1 1
      apps/app/src/components/Admin/Users/PasswordResetModal.jsx
  33. 1 1
      apps/app/src/components/Admin/Users/UserInviteModal.jsx
  34. 25 10
      apps/app/src/components/Bookmarks/BookmarkFolderItem.tsx
  35. 3 11
      apps/app/src/components/Bookmarks/BookmarkFolderNameInput.tsx
  36. 14 5
      apps/app/src/components/Bookmarks/BookmarkItem.tsx
  37. 28 33
      apps/app/src/components/Common/ClosableTextInput.tsx
  38. 3 4
      apps/app/src/components/Common/ImageCropModal.tsx
  39. 1 1
      apps/app/src/components/CompleteUserRegistrationForm.tsx
  40. 1 1
      apps/app/src/components/CreateTemplateModal.tsx
  41. 1 1
      apps/app/src/components/DeleteBookmarkFolderModal.tsx
  42. 3 2
      apps/app/src/components/EmptyTrashModal.tsx
  43. 3 56
      apps/app/src/components/ItemsTree/ItemsTree.tsx
  44. 0 51
      apps/app/src/components/Layout/Admin.module.scss
  45. 1 1
      apps/app/src/components/Me/AssociateModal.tsx
  46. 1 1
      apps/app/src/components/Me/DisassociateModal.tsx
  47. 1 1
      apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  48. 15 4
      apps/app/src/components/Navbar/PageEditorModeManager.tsx
  49. 2 2
      apps/app/src/components/Page/DisplaySwitcher.tsx
  50. 2 2
      apps/app/src/components/PageAlert/FixPageGrantAlert.tsx
  51. 1 1
      apps/app/src/components/PageAttachment/DeleteAttachmentModal.tsx
  52. 1 1
      apps/app/src/components/PageComment/DeleteCommentModal.tsx
  53. 1 1
      apps/app/src/components/PageControls/PageControls.tsx
  54. 1 1
      apps/app/src/components/PageCreateModal.tsx
  55. 1 1
      apps/app/src/components/PageDuplicateModal.tsx
  56. 1 1
      apps/app/src/components/PageEditor/GridEditModal.jsx
  57. 1 1
      apps/app/src/components/PageEditor/LinkEditModal.tsx
  58. 30 3
      apps/app/src/components/PageHeader/PageHeader.tsx
  59. 5 1
      apps/app/src/components/PageHeader/PagePathHeader.module.scss
  60. 38 38
      apps/app/src/components/PageHeader/PagePathHeader.tsx
  61. 5 1
      apps/app/src/components/PageHeader/PageTitleHeader.module.scss
  62. 34 31
      apps/app/src/components/PageHeader/PageTitleHeader.tsx
  63. 1 1
      apps/app/src/components/PageRenameModal.tsx
  64. 13 10
      apps/app/src/components/PageSelectModal/PageSelectModal.tsx
  65. 1 1
      apps/app/src/components/PrivateLegacyPages.tsx
  66. 1 1
      apps/app/src/components/PrivateLegacyPagesMigrationModal.tsx
  67. 1 1
      apps/app/src/components/PutbackPageModal.jsx
  68. 1 1
      apps/app/src/components/SearchPage/SearchOptionModal.tsx
  69. 1 1
      apps/app/src/components/ShortcutsModal.tsx
  70. 12 7
      apps/app/src/components/Sidebar/Bookmarks/BookmarkContents.tsx
  71. 72 5
      apps/app/src/components/Sidebar/PageTree/PageTreeSubstance.tsx
  72. 14 7
      apps/app/src/components/Sidebar/PageTreeItem/Ellipsis.tsx
  73. 17 13
      apps/app/src/components/Sidebar/RecentChanges/RecentChangesSubstance.tsx
  74. 7 0
      apps/app/src/components/Sidebar/Sidebar.tsx
  75. 3 2
      apps/app/src/components/Sidebar/SidebarNav/SecondaryItems.tsx
  76. 1 1
      apps/app/src/components/TemplateModal/TemplateModal.tsx
  77. 1 1
      apps/app/src/components/TreeItem/ItemNode.ts
  78. 16 18
      apps/app/src/components/TreeItem/NewPageInput/NewPageInput.tsx
  79. 1 1
      apps/app/src/features/external-user-group/client/components/ExternalUserGroup/SyncExecution.tsx
  80. 1 1
      apps/app/src/features/growi-plugin/client/components/Admin/PluginsExtensionPageContents/PluginDeleteModal.tsx
  81. 0 8
      apps/app/src/interfaces/page.ts
  82. 1 0
      apps/app/src/interfaces/sidebar-config.ts
  83. 0 1
      apps/app/src/interfaces/user-ui-settings.ts
  84. 2 1
      apps/app/src/interfaces/websocket.ts
  85. 4 0
      apps/app/src/interfaces/yjs.ts
  86. 31 12
      apps/app/src/pages/[[...path]].page.tsx
  87. 3 0
      apps/app/src/pages/_private-legacy-pages.page.tsx
  88. 5 1
      apps/app/src/pages/_search.page.tsx
  89. 4 0
      apps/app/src/pages/me/[[...path]].page.tsx
  90. 4 0
      apps/app/src/pages/tags.page.tsx
  91. 4 0
      apps/app/src/pages/trash.page.tsx
  92. 7 8
      apps/app/src/pages/user-activation.page.tsx
  93. 0 4
      apps/app/src/pages/utils/commons.ts
  94. 3 1
      apps/app/src/server/models/config.ts
  95. 0 3
      apps/app/src/server/models/user-ui-settings.ts
  96. 5 1
      apps/app/src/server/routes/apiv3/customize-setting.js
  97. 4 2
      apps/app/src/server/routes/apiv3/page/create-page.ts
  98. 57 0
      apps/app/src/server/routes/apiv3/page/get-yjs-data.ts
  99. 3 0
      apps/app/src/server/routes/apiv3/page/index.ts
  100. 2 2
      apps/app/src/server/routes/user-activation.ts

+ 44 - 1
CHANGELOG.md

@@ -1,9 +1,52 @@
 # Changelog
 # Changelog
 
 
-## [Unreleased](https://github.com/weseek/growi/compare/v7.0.2...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v7.0.3...HEAD)
 
 
 *Please do not manually update this file. We've automated the process.*
 *Please do not manually update this file. We've automated the process.*
 
 
+## [v7.0.3](https://github.com/weseek/growi/compare/v7.0.2...v7.0.3) - 2024-05-01
+
+### 🚀 Improvement
+
+* imprv: Rename on blur (#8777) @yuki-takei
+* imprv: Re-calculate max-width for page tile on editor (#8775) @yuki-takei
+* imprv: Truncate page path title in editor (#8726) @reiji-h
+* imprv: FootstrampIcon uses material symbols (#8699) @kazutoweseek
+* imprv: behaviour when pressing enter after numerical input (ex: 2.3.4.) (#8754) @WNomunomu
+
+### 🐛 Bug Fixes
+
+* fix: An error occurred during user activation via email (#8767) @maeshinshin
+* fix: Admin customize screen (#8765) @yuki-takei
+* fix: Editor theme active line (material, nord) (#8762) @satof3
+* fix: 500 error occur when pressing the button for opening PageSelectModal (#8761) @WNomunomu
+* fix: Admin screen min-height for body (#8764) @yuki-takei
+* fix: Behaviour of table operation by enter key in editor (#8756) @WNomunomu
+* fix: EditingUserList shows user icons even when the user is not opening the editor (#8752) @miya
+* fix: Tags are not created when space key is entered (Firefox) (#8758) @miya
+* fix: Parent group selection dropdown does not work (#8759) @miya
+* fix: Codeblock overflow (#8753) @yuki-takei
+* fix:  Nord and Original-dark theme in editor (#8748) @satof3
+* fix: Material and Eclipse colors (#8749) @satof3
+* fix: Always autofocus when moving to the Editor (#8730) @reiji-h
+* fix: CopyDropdown protrudes from the screen issue (#8734) @WNomunomu
+* fix: Error on today's memo create from hotkey when memo exists (#8746) @arafubeatbox
+* fix: Editor styles (#8742) @yuki-takei
+* fix: Close the comment editor after the post (#8741) @yuki-takei
+
+### 🧰 Maintenance
+
+* ci(deps): bump typeorm from 0.3.0 to 0.3.20 (#8745) @dependabot
+* support: Make AwsFileUploader type safe (#8780) @yuki-takei
+* support: Update modal header design (#8766) @satof3
+* support: Improve types in IPage and IRevision (#8763) @yuki-takei
+* support: Editor mobile appearance (#8760) @satof3
+* support: Localize in app tags language en change ja ch revision (#8757) @kazutoweseek
+* support: Upgrade vite and vitest (#8743) @yuki-takei
+* support: Adjust design details (#8738) @satof3
+* ci(deps): bump typeorm from 0.2.32 to 0.3.0 (#8602) @dependabot
+* support: Upgrade date-fns (#8744) @yuki-takei
+
 ## [v7.0.2](https://github.com/weseek/growi/compare/v7.0.1...v7.0.2) - 2024-04-17
 ## [v7.0.2](https://github.com/weseek/growi/compare/v7.0.1...v7.0.2) - 2024-04-17
 
 
 ### 💎 Features
 ### 💎 Features

+ 16 - 0
apps/app/config/i18next.config.js

@@ -0,0 +1,16 @@
+const { Lang, AllLang } = require('@growi/core');
+
+/** @type {Lang} */
+const defaultLang = Lang.en_US;
+
+/** @type {import('i18next').InitOptions} */
+const initOptions = {
+  fallbackLng: defaultLang.toString(),
+  supportedLngs: AllLang,
+  defaultNS: 'translation',
+};
+
+module.exports = {
+  defaultLang,
+  initOptions,
+};

+ 20 - 9
apps/app/config/next-i18next.config.js

@@ -2,31 +2,41 @@ const isDev = process.env.NODE_ENV === 'development';
 
 
 const path = require('path');
 const path = require('path');
 
 
-const { AllLang, Lang } = require('@growi/core');
+const { AllLang } = require('@growi/core');
 const { isServer } = require('@growi/core/dist/utils');
 const { isServer } = require('@growi/core/dist/utils');
-const I18nextChainedBackend = isDev ? require('i18next-chained-backend').default : undefined;
-const I18NextHttpBackend = require('i18next-http-backend').default;
-const I18NextLocalStorageBackend = require('i18next-localstorage-backend').default;
+
+const { defaultLang } = require('./i18next.config');
 
 
 const HMRPlugin = isDev ? require('i18next-hmr/plugin').HMRPlugin : undefined;
 const HMRPlugin = isDev ? require('i18next-hmr/plugin').HMRPlugin : undefined;
 
 
+/** @type {import('next-i18next').UserConfig} */
 module.exports = {
 module.exports = {
-  defaultLang: Lang.en_US,
+  ...require('./i18next.config').initOptions,
+
   i18n: {
   i18n: {
-    defaultLocale: Lang.en_US,
+    defaultLocale: defaultLang.toString(),
     locales: AllLang,
     locales: AllLang,
   },
   },
-  defaultNS: 'translation',
+
   localePath: path.resolve('./public/static/locales'),
   localePath: path.resolve('./public/static/locales'),
   serializeConfig: false,
   serializeConfig: false,
+
   // eslint-disable-next-line no-nested-ternary
   // eslint-disable-next-line no-nested-ternary
   use: isDev
   use: isDev
     ? isServer()
     ? isServer()
       ? [new HMRPlugin({ webpack: { server: true } })]
       ? [new HMRPlugin({ webpack: { server: true } })]
-      : [I18nextChainedBackend, new HMRPlugin({ webpack: { client: true } })]
+      : [
+        require('i18next-chained-backend').default,
+        new HMRPlugin({ webpack: { client: true } }),
+      ]
     : [],
     : [],
   backend: {
   backend: {
-    backends: isServer() ? [] : [I18NextLocalStorageBackend, I18NextHttpBackend],
+    backends: isServer()
+      ? []
+      : [
+        require('i18next-localstorage-backend').default,
+        require('i18next-http-backend').default,
+      ],
     backendOptions: [
     backendOptions: [
       // options for i18next-localstorage-backend
       // options for i18next-localstorage-backend
       { expirationTime: isDev ? 0 : 24 * 60 * 60 * 1000 }, // 1 day in production
       { expirationTime: isDev ? 0 : 24 * 60 * 60 * 1000 }, // 1 day in production
@@ -34,4 +44,5 @@ module.exports = {
       { loadPath: '/static/locales/{{lng}}/{{ns}}.json' },
       { loadPath: '/static/locales/{{lng}}/{{ns}}.json' },
     ],
     ],
   },
   },
+
 };
 };

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

@@ -10,7 +10,7 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 ------------------------------------------------
 
 
-* [`7.0.2`, `7.0`, `7`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v7.0.2/apps/app/docker/Dockerfile)
+* [`7.0.3`, `7.0`, `7`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v7.0.3/apps/app/docker/Dockerfile)
 * [`6.3.2`, `6.3`, `6` (Dockerfile)](https://github.com/weseek/growi/blob/v6.3.2/apps/app/docker/Dockerfile)
 * [`6.3.2`, `6.3`, `6` (Dockerfile)](https://github.com/weseek/growi/blob/v6.3.2/apps/app/docker/Dockerfile)
 * [`6.2.4`, `6.2` (Dockerfile)](https://github.com/weseek/growi/blob/v6.2.4/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)
 * [`6.1.15`, `6.1` (Dockerfile)](https://github.com/weseek/growi/blob/v6.1.15/apps/app/docker/Dockerfile)

+ 6 - 5
apps/app/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/app",
   "name": "@growi/app",
-  "version": "7.0.3-RC.0",
+  "version": "7.0.4-RC.0",
   "license": "MIT",
   "license": "MIT",
   "scripts": {
   "scripts": {
     "//// for production": "",
     "//// for production": "",
@@ -108,7 +108,7 @@
     "detect-indent": "^7.0.0",
     "detect-indent": "^7.0.0",
     "diff": "^5.0.0",
     "diff": "^5.0.0",
     "diff_match_patch": "^0.1.1",
     "diff_match_patch": "^0.1.1",
-    "ejs": "^3.1.8",
+    "ejs": "^3.1.10",
     "esa-node": "^0.2.2",
     "esa-node": "^0.2.2",
     "escape-string-regexp": "^4.0.0",
     "escape-string-regexp": "^4.0.0",
     "eslint-plugin-regex": "^1.8.0",
     "eslint-plugin-regex": "^1.8.0",
@@ -125,9 +125,7 @@
     "helmet": "^4.6.0",
     "helmet": "^4.6.0",
     "http-errors": "^2.0.0",
     "http-errors": "^2.0.0",
     "i18next": "^23.10.1",
     "i18next": "^23.10.1",
-    "i18next-chained-backend": "^4.6.2",
-    "i18next-http-backend": "^2.5.0",
-    "i18next-localstorage-backend": "^4.2.0",
+    "i18next-resources-to-backend": "^1.2.1",
     "is-absolute-url": "^4.0.1",
     "is-absolute-url": "^4.0.1",
     "is-iso-date": "^0.0.1",
     "is-iso-date": "^0.0.1",
     "ldapjs": "^3.0.2",
     "ldapjs": "^3.0.2",
@@ -253,7 +251,10 @@
     "fslightbox-react": "^1.7.6",
     "fslightbox-react": "^1.7.6",
     "handsontable": "=6.2.2",
     "handsontable": "=6.2.2",
     "happy-dom": "^13.2.0",
     "happy-dom": "^13.2.0",
+    "i18next-chained-backend": "^4.6.2",
     "i18next-hmr": "^3.0.4",
     "i18next-hmr": "^3.0.4",
+    "i18next-http-backend": "^2.5.0",
+    "i18next-localstorage-backend": "^4.2.0",
     "jest": "^29.5.0",
     "jest": "^29.5.0",
     "jest-date-mock": "^1.0.8",
     "jest-date-mock": "^1.0.8",
     "jest-localstorage-mock": "^2.4.14",
     "jest-localstorage-mock": "^2.4.14",

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

@@ -863,5 +863,8 @@
     "show_wip_page": "Show WIP",
     "show_wip_page": "Show WIP",
     "size_s": "Size: S",
     "size_s": "Size: S",
     "size_l": "Size: L"
     "size_l": "Size: L"
+  },
+  "create_page": {
+    "untitled": "Untitled"
   }
   }
 }
 }

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

@@ -896,5 +896,8 @@
     "show_wip_page": "WIP を表示",
     "show_wip_page": "WIP を表示",
     "size_s": "サイズ: S",
     "size_s": "サイズ: S",
     "size_l": "サイズ: L"
     "size_l": "サイズ: L"
+  },
+  "create_page": {
+    "untitled": "無題のページ"
   }
   }
 }
 }

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

@@ -866,5 +866,8 @@
     "show_wip_page": "显示 WIP",
     "show_wip_page": "显示 WIP",
     "size_s": "尺寸: S",
     "size_s": "尺寸: S",
     "size_l": "尺寸: L"
     "size_l": "尺寸: L"
+  },
+  "create_page": {
+    "untitled": "Untitled"
   }
   }
 }
 }

+ 0 - 28
apps/app/src/client/services/side-effects/yjs-draft.ts

@@ -1,28 +0,0 @@
-import { useCallback, useEffect } from 'react';
-
-import { useGlobalSocket } from '@growi/core/dist/swr';
-
-import type { CurrentPageYjsDraft } from '~/interfaces/page';
-import { SocketEventName } from '~/interfaces/websocket';
-import { useCurrentPageYjsDraft } from '~/stores/page';
-
-export const useYjsDraftEffect = (): void => {
-  const { mutate: mutateeCurrentPageYjsDraft } = useCurrentPageYjsDraft();
-  const { data: socket } = useGlobalSocket();
-
-  const yjsDraftUpdateHandler = useCallback(((currentPageYjsDraft: CurrentPageYjsDraft) => {
-    mutateeCurrentPageYjsDraft(currentPageYjsDraft);
-  }), [mutateeCurrentPageYjsDraft]);
-
-  useEffect(() => {
-
-    if (socket == null) { return }
-
-    socket.on(SocketEventName.YjsUpdated, yjsDraftUpdateHandler);
-
-    return () => {
-      socket.off(SocketEventName.YjsUpdated, yjsDraftUpdateHandler);
-    };
-
-  }, [mutateeCurrentPageYjsDraft, socket, yjsDraftUpdateHandler]);
-};

+ 33 - 0
apps/app/src/client/services/side-effects/yjs.ts

@@ -0,0 +1,33 @@
+import { useCallback, useEffect } from 'react';
+
+import { useGlobalSocket } from '@growi/core/dist/swr';
+
+import { SocketEventName } from '~/interfaces/websocket';
+import { useCurrentPageYjsData } from '~/stores/yjs';
+
+export const useCurrentPageYjsDataEffect = (): void => {
+  const { data: socket } = useGlobalSocket();
+  const { updateHasRevisionBodyDiff, updateAwarenessStateSize } = useCurrentPageYjsData();
+
+  const hasRevisionBodyDiffUpdateHandler = useCallback((hasRevisionBodyDiff: boolean) => {
+    updateHasRevisionBodyDiff(hasRevisionBodyDiff);
+  }, [updateHasRevisionBodyDiff]);
+
+  const awarenessStateSizeUpdateHandler = useCallback(((awarenessStateSize: number) => {
+    updateAwarenessStateSize(awarenessStateSize);
+  }), [updateAwarenessStateSize]);
+
+  useEffect(() => {
+
+    if (socket == null) { return }
+
+    socket.on(SocketEventName.YjsHasRevisionBodyDiffUpdated, hasRevisionBodyDiffUpdateHandler);
+    socket.on(SocketEventName.YjsAwarenessStateSizeUpdated, awarenessStateSizeUpdateHandler);
+
+    return () => {
+      socket.off(SocketEventName.YjsHasRevisionBodyDiffUpdated, hasRevisionBodyDiffUpdateHandler);
+      socket.off(SocketEventName.YjsAwarenessStateSizeUpdated, awarenessStateSizeUpdateHandler);
+    };
+
+  }, [socket, awarenessStateSizeUpdateHandler, hasRevisionBodyDiffUpdateHandler]);
+};

+ 3 - 3
apps/app/src/client/util/locale-utils.ts

@@ -2,7 +2,7 @@ import type { IncomingHttpHeaders } from 'http';
 
 
 import { Lang } from '@growi/core';
 import { Lang } from '@growi/core';
 
 
-import * as nextI18NextConfig from '^/config/next-i18next.config';
+import { defaultLang } from '^/config/i18next.config';
 
 
 // https://docs.google.com/spreadsheets/d/1FoYdyEraEQuWofzbYCDPKN7EdKgS_2ZrsDrOA8scgwQ
 // https://docs.google.com/spreadsheets/d/1FoYdyEraEQuWofzbYCDPKN7EdKgS_2ZrsDrOA8scgwQ
 const DIAGRAMS_NET_LANG_MAP = {
 const DIAGRAMS_NET_LANG_MAP = {
@@ -31,7 +31,7 @@ const getPreferredLanguage = (sortedAcceptLanguagesArray: string[]): Lang => {
     const matchingLang = Object.keys(ACCEPT_LANG_MAP).find(key => lang.includes(key));
     const matchingLang = Object.keys(ACCEPT_LANG_MAP).find(key => lang.includes(key));
     if (matchingLang) return ACCEPT_LANG_MAP[matchingLang];
     if (matchingLang) return ACCEPT_LANG_MAP[matchingLang];
   }
   }
-  return nextI18NextConfig.defaultLang;
+  return defaultLang;
 };
 };
 
 
 /**
 /**
@@ -44,7 +44,7 @@ export const detectLocaleFromBrowserAcceptLanguage = (headers: IncomingHttpHeade
   const acceptLanguages = headers['accept-language'];
   const acceptLanguages = headers['accept-language'];
 
 
   if (acceptLanguages == null) {
   if (acceptLanguages == null) {
-    return nextI18NextConfig.defaultLang;
+    return defaultLang;
   }
   }
 
 
   // 1. trim blank spaces.
   // 1. trim blank spaces.

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

@@ -1,4 +1,5 @@
-import React, { FC } from 'react';
+import type { FC } from 'react';
+import React from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import {
 import {
@@ -31,8 +32,8 @@ export const ConfirmModal: FC<ConfirmModalProps> = (props: ConfirmModalProps) =>
 
 
   return (
   return (
     <Modal isOpen={props.isModalOpen} toggle={onCancel}>
     <Modal isOpen={props.isModalOpen} toggle={onCancel}>
-      <ModalHeader tag="h4" toggle={onCancel} className="bg-danger">
-        <span className="material-symbols-outlined">help</span>
+      <ModalHeader tag="h4" toggle={onCancel} className="text-danger">
+        <span className="material-symbols-outlined me-1">warning</span>
         {t('Warning')}
         {t('Warning')}
       </ModalHeader>
       </ModalHeader>
       <ModalBody>
       <ModalBody>

+ 17 - 0
apps/app/src/components/Admin/Common/AdminNavigation.module.scss

@@ -0,0 +1,17 @@
+// button layout
+.admin-navigation {
+  &:global {
+    & > a + a {
+      margin-top: 2px;
+    }
+  }
+}
+
+// sticky settings
+.admin-navigation {
+  &:global {
+    &.sticky-top {
+      top: 30px;
+    }
+  }
+}

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

@@ -7,6 +7,11 @@ import urljoin from 'url-join';
 
 
 import { useGrowiCloudUri, useGrowiAppIdForGrowiCloud } from '../../../stores/context';
 import { useGrowiCloudUri, useGrowiAppIdForGrowiCloud } from '../../../stores/context';
 
 
+import styles from './AdminNavigation.module.scss';
+
+const moduleClass = styles['admin-navigation'];
+
+
 // eslint-disable-next-line react/prop-types
 // eslint-disable-next-line react/prop-types
 const MenuLabel = ({ menu }: { menu: string }) => {
 const MenuLabel = ({ menu }: { menu: string }) => {
   const { t } = useTranslation(['admin', 'commons']);
   const { t } = useTranslation(['admin', 'commons']);
@@ -82,7 +87,7 @@ export const AdminNavigation = (): JSX.Element => {
 
 
   }, [pathname]);
   }, [pathname]);
 
 
-  const getListGroupItemOrDropdownItemList = (isListGroupItems: boolean) => {
+  const getListGroupItemOrDropdownItemList = useCallback((isListGroupItems: boolean) => {
     return (
     return (
       <>
       <>
         {/* eslint-disable no-multi-spaces */}
         {/* eslint-disable no-multi-spaces */}
@@ -115,12 +120,12 @@ export const AdminNavigation = (): JSX.Element => {
         {/* eslint-enable no-multi-spaces */}
         {/* eslint-enable no-multi-spaces */}
       </>
       </>
     );
     );
-  };
+  }, [growiAppIdForGrowiCloud, growiCloudUri, isActiveMenu, pathname]);
 
 
   return (
   return (
     <React.Fragment>
     <React.Fragment>
       {/* List group */}
       {/* List group */}
-      <div className="list-group admin-navigation sticky-top d-none d-lg-block">
+      <div className={`list-group ${moduleClass} sticky-top d-none d-lg-block`}>
         {getListGroupItemOrDropdownItemList(true)}
         {getListGroupItemOrDropdownItemList(true)}
       </div>
       </div>
 
 

+ 17 - 8
apps/app/src/components/Admin/Customize/CustomizeLayoutSetting.tsx

@@ -30,7 +30,6 @@ const CustomizeLayoutSetting = (): JSX.Element => {
   const { resolvedTheme } = useNextThemes();
   const { resolvedTheme } = useNextThemes();
 
 
   const { isContainerFluid, setIsContainerFluid, updateLayoutSetting } = useIsContainerFluid();
   const { isContainerFluid, setIsContainerFluid, updateLayoutSetting } = useIsContainerFluid();
-  const [retrieveError, setRetrieveError] = useState<any>();
 
 
   const onClickSubmit = useCallback(async() => {
   const onClickSubmit = useCallback(async() => {
     if (isContainerFluid == null) { return }
     if (isContainerFluid == null) { return }
@@ -58,14 +57,19 @@ const CustomizeLayoutSetting = (): JSX.Element => {
           <h2 className="admin-setting-header">{t('customize_settings.layout')}</h2>
           <h2 className="admin-setting-header">{t('customize_settings.layout')}</h2>
 
 
           <div className="d-flex justify-content-around mt-5">
           <div className="d-flex justify-content-around mt-5">
-            <div id="layoutOptions" className="row row-cols-2">
+            <div className="row row-cols-2">
               <div className="col">
               <div className="col">
                 <div
                 <div
-                  className={`card customize-layout-card ${!isContainerFluid ? 'border-active' : ''}`}
+                  className={`card border border-4 ${!isContainerFluid ? 'border-primary' : ''}`}
                   onClick={() => setIsContainerFluid(false)}
                   onClick={() => setIsContainerFluid(false)}
                   role="button"
                   role="button"
                 >
                 >
-                  <img src={`/images/customize-settings/default-${resolvedTheme}.svg`} />
+                  {/* eslint-disable-next-line @next/next/no-img-element */}
+                  <img
+                    className="card-img-top"
+                    src={`/images/customize-settings/default-${resolvedTheme}.svg`}
+                    alt={t('customize_settings.layout_options.default')}
+                  />
                   <div className="card-body text-center">
                   <div className="card-body text-center">
                     {t('customize_settings.layout_options.default')}
                     {t('customize_settings.layout_options.default')}
                   </div>
                   </div>
@@ -73,12 +77,17 @@ const CustomizeLayoutSetting = (): JSX.Element => {
               </div>
               </div>
               <div className="col">
               <div className="col">
                 <div
                 <div
-                  className={`card customize-layout-card ${isContainerFluid ? 'border-active' : ''}`}
+                  className={`card border border-4 ${isContainerFluid ? 'border-primary' : ''}`}
                   onClick={() => setIsContainerFluid(true)}
                   onClick={() => setIsContainerFluid(true)}
                   role="button"
                   role="button"
                 >
                 >
-                  <img src={`/images/customize-settings/fluid-${resolvedTheme}.svg`} />
-                  <div className="card-body  text-center">
+                  {/* eslint-disable-next-line @next/next/no-img-element */}
+                  <img
+                    className="card-img-top"
+                    src={`/images/customize-settings/fluid-${resolvedTheme}.svg`}
+                    alt={t('customize_settings.layout_options.expanded')}
+                  />
+                  <div className="card-body text-center">
                     {t('customize_settings.layout_options.expanded')}
                     {t('customize_settings.layout_options.expanded')}
                   </div>
                   </div>
                 </div>
                 </div>
@@ -88,7 +97,7 @@ const CustomizeLayoutSetting = (): JSX.Element => {
 
 
           <div className="row my-3">
           <div className="row my-3">
             <div className="mx-auto">
             <div className="mx-auto">
-              <button type="button" className="btn btn-primary" onClick={onClickSubmit} disabled={retrieveError != null}>{ t('Update') }</button>
+              <button type="button" className="btn btn-primary" onClick={onClickSubmit}>{ t('Update') }</button>
             </div>
             </div>
           </div>
           </div>
         </div>
         </div>

+ 21 - 12
apps/app/src/components/Admin/Customize/CustomizeSidebarSetting.tsx

@@ -1,5 +1,6 @@
 import React, { useCallback } from 'react';
 import React, { useCallback } from 'react';
 
 
+import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { Card, CardBody } from 'reactstrap';
 import { Card, CardBody } from 'reactstrap';
 
 
@@ -11,7 +12,7 @@ const CustomizeSidebarsetting = (): JSX.Element => {
   const { t } = useTranslation(['admin', 'commons']);
   const { t } = useTranslation(['admin', 'commons']);
 
 
   const {
   const {
-    update, isSidebarCollapsedMode, setIsSidebarCollapsedMode,
+    data, update, setIsSidebarCollapsedMode, setIsSidebarClosedAtDockMode,
   } = useSWRxSidebarConfig();
   } = useSWRxSidebarConfig();
 
 
   const { resolvedTheme } = useNextThemes();
   const { resolvedTheme } = useNextThemes();
@@ -28,6 +29,12 @@ const CustomizeSidebarsetting = (): JSX.Element => {
     }
     }
   }, [t, update]);
   }, [t, update]);
 
 
+  if (data == null) {
+    return <LoadingSpinner />;
+  }
+
+  const { isSidebarCollapsedMode, isSidebarClosedAtDockMode } = data;
+
   return (
   return (
     <React.Fragment>
     <React.Fragment>
       <div className="row">
       <div className="row">
@@ -42,14 +49,15 @@ const CustomizeSidebarsetting = (): JSX.Element => {
           </Card>
           </Card>
 
 
           <div className="d-flex justify-content-around mt-5">
           <div className="d-flex justify-content-around mt-5">
-            <div id="layoutOptions" className="row row-cols-2">
+            <div className="row row-cols-2">
               <div className="col">
               <div className="col">
                 <div
                 <div
-                  className={`card customize-layout-card ${isSidebarCollapsedMode ? 'border-active' : ''}`}
+                  className={`card border border-4 ${isSidebarCollapsedMode ? 'border-primary' : ''}`}
                   onClick={() => setIsSidebarCollapsedMode(true)}
                   onClick={() => setIsSidebarCollapsedMode(true)}
                   role="button"
                   role="button"
                 >
                 >
-                  <img src={drawerIconFileName} />
+                  {/* eslint-disable-next-line @next/next/no-img-element */}
+                  <img src={drawerIconFileName} alt="Drawer Mode" />
                   <div className="card-body text-center">
                   <div className="card-body text-center">
                     Drawer Mode
                     Drawer Mode
                   </div>
                   </div>
@@ -57,11 +65,12 @@ const CustomizeSidebarsetting = (): JSX.Element => {
               </div>
               </div>
               <div className="col">
               <div className="col">
                 <div
                 <div
-                  className={`card customize-layout-card ${!isSidebarCollapsedMode ? 'border-active' : ''}`}
+                  className={`card border border-4 ${!isSidebarCollapsedMode ? 'border-primary' : ''}`}
                   onClick={() => setIsSidebarCollapsedMode(false)}
                   onClick={() => setIsSidebarCollapsedMode(false)}
                   role="button"
                   role="button"
                 >
                 >
-                  <img src={dockIconFileName} />
+                  {/* eslint-disable-next-line @next/next/no-img-element */}
+                  <img src={dockIconFileName} alt="Dock Mode" />
                   <div className="card-body  text-center">
                   <div className="card-body  text-center">
                     Dock Mode
                     Dock Mode
                   </div>
                   </div>
@@ -82,9 +91,9 @@ const CustomizeSidebarsetting = (): JSX.Element => {
                 type="radio"
                 type="radio"
                 id="is-open"
                 id="is-open"
                 className="form-check-input"
                 className="form-check-input"
-                name="mailVisibility"
-                checked={isSidebarCollapsedMode === false}
-                onChange={() => setIsSidebarCollapsedMode(false)}
+                checked={isSidebarCollapsedMode === false && isSidebarClosedAtDockMode === false}
+                disabled={isSidebarCollapsedMode}
+                onChange={() => setIsSidebarClosedAtDockMode(false)}
               />
               />
               <label className="form-label form-check-label" htmlFor="is-open">
               <label className="form-label form-check-label" htmlFor="is-open">
                 {t('customize_settings.default_sidebar_mode.dock_mode_default_open')}
                 {t('customize_settings.default_sidebar_mode.dock_mode_default_open')}
@@ -95,9 +104,9 @@ const CustomizeSidebarsetting = (): JSX.Element => {
                 type="radio"
                 type="radio"
                 id="is-closed"
                 id="is-closed"
                 className="form-check-input"
                 className="form-check-input"
-                name="mailVisibility"
-                checked={isSidebarCollapsedMode === true}
-                onChange={() => setIsSidebarCollapsedMode(true)}
+                checked={isSidebarCollapsedMode === false && isSidebarClosedAtDockMode === true}
+                disabled={isSidebarCollapsedMode}
+                onChange={() => setIsSidebarClosedAtDockMode(true)}
               />
               />
               <label className="form-label form-check-label" htmlFor="is-closed">
               <label className="form-label form-check-label" htmlFor="is-closed">
                 {t('customize_settings.default_sidebar_mode.dock_mode_default_close')}
                 {t('customize_settings.default_sidebar_mode.dock_mode_default_close')}

+ 7 - 4
apps/app/src/components/Admin/Customize/CustomizeThemeOptions.tsx

@@ -25,11 +25,12 @@ const CustomizeThemeOptions = (props: Props): JSX.Element => {
   }, [availableThemes]);
   }, [availableThemes]);
 
 
   return (
   return (
-    <div id="themeOptions">
+    <>
+
       {/* Light and Dark Themes */}
       {/* Light and Dark Themes */}
       <div>
       <div>
         <h3>{t('customize_settings.theme_desc.light_and_dark')}</h3>
         <h3>{t('customize_settings.theme_desc.light_and_dark')}</h3>
-        <div className="d-flex flex-wrap">
+        <div className="hstack gap-3">
           {lightNDarkThemes.map((theme) => {
           {lightNDarkThemes.map((theme) => {
             return (
             return (
               <ThemeColorBox
               <ThemeColorBox
@@ -42,10 +43,11 @@ const CustomizeThemeOptions = (props: Props): JSX.Element => {
           })}
           })}
         </div>
         </div>
       </div>
       </div>
+
       {/* Only one mode Theme */}
       {/* Only one mode Theme */}
       <div className="mt-3">
       <div className="mt-3">
         <h3>{t('customize_settings.theme_desc.unique')}</h3>
         <h3>{t('customize_settings.theme_desc.unique')}</h3>
-        <div className="d-flex flex-wrap">
+        <div className="hstack gap-3">
           {oneModeThemes.map((theme) => {
           {oneModeThemes.map((theme) => {
             return (
             return (
               <ThemeColorBox
               <ThemeColorBox
@@ -58,7 +60,8 @@ const CustomizeThemeOptions = (props: Props): JSX.Element => {
           })}
           })}
         </div>
         </div>
       </div>
       </div>
-    </div>
+
+    </>
   );
   );
 
 
 };
 };

+ 6 - 0
apps/app/src/components/Admin/Customize/ThemeColorBox.module.scss

@@ -0,0 +1,6 @@
+@use '@growi/core/scss/bootstrap/init' as bs;
+
+// layout
+.theme-option-container :global {
+  min-width: 100px;
+}

+ 15 - 5
apps/app/src/components/Admin/Customize/ThemeColorBox.tsx

@@ -2,6 +2,10 @@ import React from 'react';
 
 
 import type { GrowiThemeMetadata } from '@growi/core';
 import type { GrowiThemeMetadata } from '@growi/core';
 
 
+import styles from './ThemeColorBox.module.scss';
+
+const themeOptionClass = styles['theme-option-container'];
+
 
 
 type Props = {
 type Props = {
   isSelected: boolean,
   isSelected: boolean,
@@ -19,13 +23,19 @@ export const ThemeColorBox = (props: Props): JSX.Element => {
   } = metadata;
   } = metadata;
 
 
   return (
   return (
-    // TODO: Display a primary color border when icon is selected
     <div
     <div
       id={`theme-option-${name}`}
       id={`theme-option-${name}`}
-      className={`theme-option-container d-flex flex-column align-items-center ${isSelected ? 'active' : ''}`}
+      className={`${themeOptionClass} d-flex flex-column align-items-center ${isSelected ? 'active' : ''}`}
       onClick={onSelected}
       onClick={onSelected}
     >
     >
-      <a id={name} role="button" className={`m-0 rounded ${name} theme-button`}>
+      <a
+        id={name}
+        role="button"
+        className={`
+          m-0 rounded rounded-3
+          border border-4 border-primary ${isSelected ? '' : 'border-opacity-10'}`
+        }
+      >
         <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" className="rounded">
         <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" className="rounded">
           <path d="M32.5,0V36.364L64,20.437V0Z" fill={lightBg} />
           <path d="M32.5,0V36.364L64,20.437V0Z" fill={lightBg} />
           <path d="M32.5,36.364V64H64V20.438Z" fill={darkBg} />
           <path d="M32.5,36.364V64H64V20.438Z" fill={darkBg} />
@@ -45,8 +55,8 @@ export const ThemeColorBox = (props: Props): JSX.Element => {
           <rect width="19.629" height="2.062" transform="translate(6.436 53.439)" fill={darkIcon} />
           <rect width="19.629" height="2.062" transform="translate(6.436 53.439)" fill={darkIcon} />
         </svg>
         </svg>
       </a>
       </a>
-      <span className="theme-option-name mt-2"><b>{ name }</b></span>
-      { !isPresetTheme && <span className="theme-option-badge badge bg-primary mt-1">Plugin</span> }
+      <span className={`mt-2 ${isSelected ? '' : 'opacity-25'}`}><b>{ name }</b></span>
+      { !isPresetTheme && <span className={`badge bg-primary mt-1 ${isSelected ? '' : 'opacity-25'}`}>Plugin</span> }
     </div>
     </div>
   );
   );
 
 

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

@@ -157,7 +157,7 @@ const SelectCollectionsModal = (props: Props): JSX.Element => {
 
 
   return (
   return (
     <Modal isOpen={isOpen} toggle={onClose}>
     <Modal isOpen={isOpen} toggle={onClose}>
-      <ModalHeader tag="h4" toggle={onClose} className="bg-info text-light">
+      <ModalHeader tag="h4" toggle={onClose} className="text-info">
         {t('admin:export_management.export_collections')}
         {t('admin:export_management.export_collections')}
       </ModalHeader>
       </ModalHeader>
 
 

+ 1 - 1
apps/app/src/components/Admin/ImportData/GrowiArchive/ErrorViewer.tsx

@@ -21,7 +21,7 @@ const ErrorViewer = (props: ErrorViewerProps): JSX.Element => {
 
 
   return (
   return (
     <Modal isOpen={props.isOpen} toggle={props.onClose} size="lg">
     <Modal isOpen={props.isOpen} toggle={props.onClose} size="lg">
-      <ModalHeader tag="h4" toggle={props.onClose} className="bg-danger text-light">
+      <ModalHeader tag="h4" toggle={props.onClose} className="text-danger">
         Errors
         Errors
       </ModalHeader>
       </ModalHeader>
       <ModalBody>
       <ModalBody>

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

@@ -190,7 +190,7 @@ class ImportCollectionConfigurationModal extends React.Component {
 
 
     return (
     return (
       <Modal isOpen={this.props.isOpen} toggle={this.props.onClose} onEnter={this.initialize}>
       <Modal isOpen={this.props.isOpen} toggle={this.props.onClose} onEnter={this.initialize}>
-        <ModalHeader tag="h4" toggle={this.props.onClose} className="bg-info text-light">
+        <ModalHeader tag="h4" toggle={this.props.onClose} className="text-info">
           {`'${collectionName}'`} Configuration
           {`'${collectionName}'`} Configuration
         </ModalHeader>
         </ModalHeader>
 
 

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

@@ -12,7 +12,7 @@ class NotificationDeleteModal extends React.PureComponent {
     const { t, notificationForConfiguration } = this.props;
     const { t, notificationForConfiguration } = this.props;
     return (
     return (
       <Modal isOpen={this.props.isOpen} toggle={this.props.onClose}>
       <Modal isOpen={this.props.isOpen} toggle={this.props.onClose}>
-        <ModalHeader tag="h4" toggle={this.props.onClose} className="bg-danger text-light">
+        <ModalHeader tag="h4" toggle={this.props.onClose} className="text-danger">
           <span className="material-symbols-outlined">delete_forever</span>Delete Global Notification Setting
           <span className="material-symbols-outlined">delete_forever</span>Delete Global Notification Setting
         </ModalHeader>
         </ModalHeader>
         <ModalBody>
         <ModalBody>

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

@@ -20,7 +20,7 @@ const DeleteAllShareLinksModal = React.memo((props) => {
 
 
   return (
   return (
     <Modal isOpen={props.isOpen} toggle={closeButtonHandler} className="page-comment-delete-modal">
     <Modal isOpen={props.isOpen} toggle={closeButtonHandler} className="page-comment-delete-modal">
-      <ModalHeader tag="h4" toggle={closeButtonHandler} className="bg-danger text-light">
+      <ModalHeader tag="h4" toggle={closeButtonHandler} className="text-danger">
         <span>
         <span>
           <span className="material-symbols-outlined">delete_forever</span>
           <span className="material-symbols-outlined">delete_forever</span>
           {t('security_settings.delete_all_share_links')}
           {t('security_settings.delete_all_share_links')}

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

@@ -44,7 +44,7 @@ class LdapAuthTestModal extends React.Component {
 
 
     return (
     return (
       <Modal isOpen={this.props.isOpen} toggle={this.props.onClose}>
       <Modal isOpen={this.props.isOpen} toggle={this.props.onClose}>
-        <ModalHeader tag="h4" toggle={this.props.onClose} className="bg-info text-light">
+        <ModalHeader tag="h4" toggle={this.props.onClose} className="text-info">
           Test LDAP Account
           Test LDAP Account
         </ModalHeader>
         </ModalHeader>
         <ModalBody>
         <ModalBody>

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

@@ -25,7 +25,7 @@ const ConfirmBotChangeModal = (props) => {
     <Modal isOpen={props.isOpen} centered>
     <Modal isOpen={props.isOpen} centered>
       <ModalHeader
       <ModalHeader
         toggle={handleCancelButton}
         toggle={handleCancelButton}
-        className="bg-danger"
+        className="text-danger"
       >
       >
         {t('slack_integration.modal.warning')}
         {t('slack_integration.modal.warning')}
       </ModalHeader>
       </ModalHeader>

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

@@ -31,7 +31,7 @@ export const DeleteSlackBotSettingsModal = React.memo((props: DeleteSlackBotSett
 
 
   return (
   return (
     <Modal isOpen={isOpen} toggle={closeButtonHandler} className="page-comment-delete-modal">
     <Modal isOpen={isOpen} toggle={closeButtonHandler} className="page-comment-delete-modal">
-      <ModalHeader tag="h4" toggle={closeButtonHandler} className="bg-danger text-light">
+      <ModalHeader tag="h4" toggle={closeButtonHandler} className="text-danger">
         <span>
         <span>
           {isResetAll && (
           {isResetAll && (
             <>
             <>

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

@@ -177,7 +177,7 @@ export const UserGroupDeleteModal: FC<Props> = (props: Props) => {
 
 
   return (
   return (
     <Modal className="modal-md" isOpen={props.isShow} toggle={toggleHandler}>
     <Modal className="modal-md" isOpen={props.isShow} toggle={toggleHandler}>
-      <ModalHeader tag="h4" toggle={toggleHandler} className="bg-danger text-light">
+      <ModalHeader tag="h4" toggle={toggleHandler}>
         <span className="material-symbols-outlined">delete_forever</span> {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>
       </ModalHeader>
       <ModalBody>
       <ModalBody>

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

@@ -69,7 +69,7 @@ export const UserGroupModal: FC<Props> = (props: Props) => {
   return (
   return (
     <Modal className="modal-md" isOpen={isShow} toggle={onHide}>
     <Modal className="modal-md" isOpen={isShow} toggle={onHide}>
       <form onSubmit={onSubmitHandler}>
       <form onSubmit={onSubmitHandler}>
-        <ModalHeader tag="h4" toggle={onHide} className="bg-primary text-light">
+        <ModalHeader tag="h4" toggle={onHide}>
           {t('user_group_management.basic_info')}
           {t('user_group_management.basic_info')}
         </ModalHeader>
         </ModalHeader>
 
 

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

@@ -27,7 +27,7 @@ export const UpdateParentConfirmModal: FC = () => {
 
 
   return (
   return (
     <Modal className="modal-md" isOpen={isOpened} toggle={closeModal}>
     <Modal className="modal-md" isOpen={isOpened} toggle={closeModal}>
-      <ModalHeader tag="h4" toggle={closeModal} className="bg-warning text-light">
+      <ModalHeader tag="h4" toggle={closeModal} className="text-warning">
         <span className="material-symbols-outlined">warning</span> {t('admin:user_group_management.update_parent_confirm_modal.header')}
         <span className="material-symbols-outlined">warning</span> {t('admin:user_group_management.update_parent_confirm_modal.header')}
       </ModalHeader>
       </ModalHeader>
       {
       {

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

@@ -45,7 +45,7 @@ const UserGroupUserModal = (props: Props): JSX.Element => {
 
 
   return (
   return (
     <Modal isOpen={isOpen} toggle={onClose}>
     <Modal isOpen={isOpen} toggle={onClose}>
-      <ModalHeader tag="h4" toggle={onClose} className="bg-info text-light">
+      <ModalHeader tag="h4" toggle={onClose} className="text-info">
         {t('admin:user_group_management.add_modal.add_user') }
         {t('admin:user_group_management.add_modal.add_user') }
       </ModalHeader>
       </ModalHeader>
       <ModalBody>
       <ModalBody>

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

@@ -187,7 +187,7 @@ class PasswordResetModal extends React.Component {
 
 
     return (
     return (
       <Modal isOpen={this.props.isOpen} toggle={this.props.onClose}>
       <Modal isOpen={this.props.isOpen} toggle={this.props.onClose}>
-        <ModalHeader tag="h4" toggle={this.props.onClose} className="bg-warning text-light">
+        <ModalHeader tag="h4" toggle={this.props.onClose} className="text-warning">
           {t('user_management.reset_password') }
           {t('user_management.reset_password') }
         </ModalHeader>
         </ModalHeader>
         <ModalBody>
         <ModalBody>

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

@@ -261,7 +261,7 @@ class UserInviteModal extends React.Component {
 
 
     return (
     return (
       <Modal isOpen={adminUsersContainer.state.isUserInviteModalShown}>
       <Modal isOpen={adminUsersContainer.state.isUserInviteModalShown}>
-        <ModalHeader tag="h4" toggle={this.onToggleModal} className="bg-info text-light">
+        <ModalHeader tag="h4" toggle={this.onToggleModal} className="text-info">
           {t('admin:user_management.invite_users') }
           {t('admin:user_management.invite_users') }
         </ModalHeader>
         </ModalHeader>
         <ModalBody>
         <ModalBody>

+ 25 - 10
apps/app/src/components/Bookmarks/BookmarkFolderItem.tsx

@@ -59,23 +59,36 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
     setTargetFolder(folderId);
     setTargetFolder(folderId);
   }, [folderId, isOpen]);
   }, [folderId, isOpen]);
 
 
+  const cancel = useCallback(() => {
+    setIsRenameAction(false);
+    setIsCreateAction(false);
+  }, []);
+
   // Rename for bookmark folder handler
   // Rename for bookmark folder handler
-  const onPressEnterHandlerForRename = useCallback(async(folderName: string) => {
+  const rename = useCallback(async(folderName: string) => {
+    if (folderName.trim() === '') {
+      return cancel();
+    }
+
     try {
     try {
       // TODO: do not use any type
       // TODO: do not use any type
-      await updateBookmarkFolder(folderId, folderName, parent as any, childFolder);
+      await updateBookmarkFolder(folderId, folderName.trim(), parent as any, childFolder);
       bookmarkFolderTreeMutation();
       bookmarkFolderTreeMutation();
       setIsRenameAction(false);
       setIsRenameAction(false);
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
     }
     }
-  }, [bookmarkFolderTreeMutation, childFolder, folderId, parent]);
+  }, [bookmarkFolderTreeMutation, cancel, childFolder, folderId, parent]);
 
 
   // Create new folder / subfolder handler
   // Create new folder / subfolder handler
-  const onPressEnterHandlerForCreate = useCallback(async(folderName: string) => {
+  const create = useCallback(async(folderName: string) => {
+    if (folderName.trim() === '') {
+      return cancel();
+    }
+
     try {
     try {
-      await addNewFolder(folderName, targetFolder);
+      await addNewFolder(folderName.trim(), targetFolder);
       setIsOpen(true);
       setIsOpen(true);
       setIsCreateAction(false);
       setIsCreateAction(false);
       bookmarkFolderTreeMutation();
       bookmarkFolderTreeMutation();
@@ -83,7 +96,7 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
     }
     }
-  }, [bookmarkFolderTreeMutation, targetFolder]);
+  }, [bookmarkFolderTreeMutation, cancel, targetFolder]);
 
 
   const onClickPlusButton = useCallback(async(e) => {
   const onClickPlusButton = useCallback(async(e) => {
     e.stopPropagation();
     e.stopPropagation();
@@ -245,8 +258,9 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
           </div>
           </div>
           {isRenameAction ? (
           {isRenameAction ? (
             <BookmarkFolderNameInput
             <BookmarkFolderNameInput
-              onClickOutside={() => setIsRenameAction(false)}
-              onPressEnter={onPressEnterHandlerForRename}
+              onPressEnter={rename}
+              onBlur={rename}
+              onPressEscape={cancel}
               value={name}
               value={name}
             />
             />
           ) : (
           ) : (
@@ -290,8 +304,9 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
       {isCreateAction && (
       {isCreateAction && (
         <div className="flex-fill">
         <div className="flex-fill">
           <BookmarkFolderNameInput
           <BookmarkFolderNameInput
-            onClickOutside={() => setIsCreateAction(false)}
-            onPressEnter={onPressEnterHandlerForCreate}
+            onPressEnter={create}
+            onBlur={create}
+            onPressEscape={cancel}
           />
           />
         </div>
         </div>
       )}
       )}

+ 3 - 11
apps/app/src/components/Bookmarks/BookmarkFolderNameInput.tsx

@@ -1,29 +1,21 @@
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
 import { ValidationTarget } from '~/client/util/input-validator';
 import { ValidationTarget } from '~/client/util/input-validator';
+import type { ClosableTextInputProps } from '~/components/Common/ClosableTextInput';
 import ClosableTextInput from '~/components/Common/ClosableTextInput';
 import ClosableTextInput from '~/components/Common/ClosableTextInput';
 
 
 
 
-type Props = {
-  onClickOutside: () => void
-  onPressEnter: (folderName: string) => void
-  value?: string
-}
+type Props = ClosableTextInputProps;
 
 
 export const BookmarkFolderNameInput = (props: Props): JSX.Element => {
 export const BookmarkFolderNameInput = (props: Props): JSX.Element => {
-  const {
-    onClickOutside, onPressEnter, value,
-  } = props;
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
   return (
   return (
     <div className="flex-fill folder-name-input">
     <div className="flex-fill folder-name-input">
       <ClosableTextInput
       <ClosableTextInput
-        value={value}
         placeholder={t('bookmark_folder.input_placeholder')}
         placeholder={t('bookmark_folder.input_placeholder')}
-        onClickOutside={onClickOutside}
-        onPressEnter={onPressEnter}
         validationTarget={ValidationTarget.FOLDER}
         validationTarget={ValidationTarget.FOLDER}
+        {...props}
       />
       />
     </div>
     </div>
   );
   );

+ 14 - 5
apps/app/src/components/Bookmarks/BookmarkItem.tsx

@@ -86,9 +86,17 @@ export const BookmarkItem = (props: Props): JSX.Element => {
     setRenameInputShown(true);
     setRenameInputShown(true);
   }, []);
   }, []);
 
 
-  const pressEnterForRenameHandler = useCallback(async(inputText: string) => {
+  const cancel = useCallback(() => {
+    setRenameInputShown(false);
+  }, []);
+
+  const rename = useCallback(async(inputText: string) => {
+    if (inputText.trim() === '') {
+      return cancel();
+    }
+
     const parentPath = pathUtils.addTrailingSlash(nodePath.dirname(bookmarkedPage.path ?? ''));
     const parentPath = pathUtils.addTrailingSlash(nodePath.dirname(bookmarkedPage.path ?? ''));
-    const newPagePath = nodePath.resolve(parentPath, inputText);
+    const newPagePath = nodePath.resolve(parentPath, inputText.trim());
     if (newPagePath === bookmarkedPage.path) {
     if (newPagePath === bookmarkedPage.path) {
       setRenameInputShown(false);
       setRenameInputShown(false);
       return;
       return;
@@ -104,7 +112,7 @@ export const BookmarkItem = (props: Props): JSX.Element => {
       setRenameInputShown(true);
       setRenameInputShown(true);
       toastError(err);
       toastError(err);
     }
     }
-  }, [bookmarkedPage.path, bookmarkedPage._id, bookmarkedPage.revision, bookmarkFolderTreeMutation, mutatePageInfo]);
+  }, [bookmarkedPage.path, bookmarkedPage._id, bookmarkedPage.revision, cancel, bookmarkFolderTreeMutation, mutatePageInfo]);
 
 
   const deleteMenuItemClickHandler = useCallback(async(_pageId: string, pageInfo: IPageInfoAll | undefined): Promise<void> => {
   const deleteMenuItemClickHandler = useCallback(async(_pageId: string, pageInfo: IPageInfoAll | undefined): Promise<void> => {
     if (bookmarkedPage._id == null || bookmarkedPage.path == null) {
     if (bookmarkedPage._id == null || bookmarkedPage.path == null) {
@@ -158,8 +166,9 @@ export const BookmarkItem = (props: Props): JSX.Element => {
             <ClosableTextInput
             <ClosableTextInput
               value={nodePath.basename(bookmarkedPage.path ?? '')}
               value={nodePath.basename(bookmarkedPage.path ?? '')}
               placeholder={t('Input page name')}
               placeholder={t('Input page name')}
-              onClickOutside={() => { setRenameInputShown(false) }}
-              onPressEnter={pressEnterForRenameHandler}
+              onPressEnter={rename}
+              onBlur={rename}
+              onPressEscape={() => { setRenameInputShown(false) }}
               validationTarget={ValidationTarget.PAGE}
               validationTarget={ValidationTarget.PAGE}
             />
             />
           )
           )

+ 28 - 33
apps/app/src/components/Common/ClosableTextInput.tsx

@@ -1,6 +1,6 @@
 import type { FC } from 'react';
 import type { FC } from 'react';
 import React, {
 import React, {
-  memo, useEffect, useRef, useState,
+  memo, useCallback, useEffect, useRef, useState,
 } from 'react';
 } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
@@ -9,90 +9,85 @@ import AutosizeInput from 'react-input-autosize';
 import type { AlertInfo } from '~/client/util/input-validator';
 import type { AlertInfo } from '~/client/util/input-validator';
 import { AlertType, inputValidator } from '~/client/util/input-validator';
 import { AlertType, inputValidator } from '~/client/util/input-validator';
 
 
-type ClosableTextInputProps = {
+export type ClosableTextInputProps = {
   value?: string
   value?: string
   placeholder?: string
   placeholder?: string
   validationTarget?: string,
   validationTarget?: string,
   useAutosizeInput?: boolean
   useAutosizeInput?: boolean
   inputClassName?: string,
   inputClassName?: string,
-  onPressEnter?(inputText: string | null): void
-  onPressEscape?: () => void
-  onClickOutside?(): void
+  onPressEnter?(inputText: string): void
+  onPressEscape?(inputText: string): void
+  onBlur?(inputText: string): void
   onChange?(inputText: string): void
   onChange?(inputText: string): void
 }
 }
 
 
 const ClosableTextInput: FC<ClosableTextInputProps> = memo((props: ClosableTextInputProps) => {
 const ClosableTextInput: FC<ClosableTextInputProps> = memo((props: ClosableTextInputProps) => {
   const { t } = useTranslation();
   const { t } = useTranslation();
-  const { validationTarget } = props;
+  const {
+    validationTarget, onPressEnter, onPressEscape, onBlur, onChange,
+  } = props;
 
 
   const inputRef = useRef<HTMLInputElement>(null);
   const inputRef = useRef<HTMLInputElement>(null);
-  const [inputText, setInputText] = useState(props.value);
+  const [inputText, setInputText] = useState(props.value ?? '');
   const [currentAlertInfo, setAlertInfo] = useState<AlertInfo | null>(null);
   const [currentAlertInfo, setAlertInfo] = useState<AlertInfo | null>(null);
   const [isAbleToShowAlert, setIsAbleToShowAlert] = useState(false);
   const [isAbleToShowAlert, setIsAbleToShowAlert] = useState(false);
   const [isComposing, setComposing] = useState(false);
   const [isComposing, setComposing] = useState(false);
 
 
 
 
-  const createValidation = async(inputText: string) => {
+  const createValidation = useCallback(async(inputText: string) => {
     const alertInfo = await inputValidator(inputText, validationTarget);
     const alertInfo = await inputValidator(inputText, validationTarget);
     if (alertInfo && alertInfo.message != null && alertInfo.target != null) {
     if (alertInfo && alertInfo.message != null && alertInfo.target != null) {
       alertInfo.message = t(alertInfo.message, { target: t(alertInfo.target) });
       alertInfo.message = t(alertInfo.message, { target: t(alertInfo.target) });
     }
     }
     setAlertInfo(alertInfo);
     setAlertInfo(alertInfo);
-  };
+  }, [t, validationTarget]);
 
 
-  const onChangeHandler = async(e: React.ChangeEvent<HTMLInputElement>) => {
+  const changeHandler = useCallback(async(e: React.ChangeEvent<HTMLInputElement>) => {
     const inputText = e.target.value;
     const inputText = e.target.value;
     createValidation(inputText);
     createValidation(inputText);
     setInputText(inputText);
     setInputText(inputText);
     setIsAbleToShowAlert(true);
     setIsAbleToShowAlert(true);
 
 
-    props.onChange?.(inputText);
-  };
+    onChange?.(inputText);
+  }, [createValidation, onChange]);
 
 
-  const onFocusHandler = async(e: React.ChangeEvent<HTMLInputElement>) => {
+  const onFocusHandler = useCallback(async(e: React.ChangeEvent<HTMLInputElement>) => {
     const inputText = e.target.value;
     const inputText = e.target.value;
     await createValidation(inputText);
     await createValidation(inputText);
-  };
+  }, [createValidation]);
 
 
-  const onPressEnter = () => {
-    if (props.onPressEnter != null) {
-      const text = inputText != null ? inputText.trim() : null;
-      if (currentAlertInfo == null) {
-        props.onPressEnter(text);
-      }
+  const pressEnterHandler = useCallback(() => {
+    if (currentAlertInfo == null) {
+      onPressEnter?.(inputText.trim());
     }
     }
-  };
+  }, [currentAlertInfo, inputText, onPressEnter]);
 
 
-  const onKeyDownHandler = (e) => {
+  const onKeyDownHandler = useCallback((e) => {
     switch (e.key) {
     switch (e.key) {
       case 'Enter':
       case 'Enter':
         // Do nothing when composing
         // Do nothing when composing
         if (isComposing) {
         if (isComposing) {
           return;
           return;
         }
         }
-        onPressEnter();
+        pressEnterHandler();
         break;
         break;
       case 'Escape':
       case 'Escape':
         if (isComposing) {
         if (isComposing) {
           return;
           return;
         }
         }
-        props.onPressEscape?.();
+        onPressEscape?.(inputText.trim());
         break;
         break;
       default:
       default:
         break;
         break;
     }
     }
-  };
+  }, [inputText, isComposing, pressEnterHandler, onPressEscape]);
 
 
   /*
   /*
    * Hide when click outside the ref
    * Hide when click outside the ref
    */
    */
-  const onBlurHandler = () => {
-    if (props.onClickOutside == null) {
-      return;
-    }
-
-    props.onClickOutside();
-  };
+  const onBlurHandler = useCallback(() => {
+    onBlur?.(inputText.trim());
+  }, [inputText, onBlur]);
 
 
   // didMount
   // didMount
   useEffect(() => {
   useEffect(() => {
@@ -126,7 +121,7 @@ const ClosableTextInput: FC<ClosableTextInputProps> = memo((props: ClosableTextI
     placeholder: props.placeholder,
     placeholder: props.placeholder,
     name: 'input',
     name: 'input',
     onFocus: onFocusHandler,
     onFocus: onFocusHandler,
-    onChange: onChangeHandler,
+    onChange: changeHandler,
     onKeyDown: onKeyDownHandler,
     onKeyDown: onKeyDownHandler,
     onCompositionStart: () => setComposing(true),
     onCompositionStart: () => setComposing(true),
     onCompositionEnd: () => setComposing(false),
     onCompositionEnd: () => setComposing(false),

+ 3 - 4
apps/app/src/components/Common/ImageCropModal.tsx

@@ -1,6 +1,5 @@
-import React, {
-  FC, useCallback, useEffect, useState,
-} from 'react';
+import type { FC } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
 
 
 import canvasToBlob from 'async-canvas-to-blob';
 import canvasToBlob from 'async-canvas-to-blob';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
@@ -137,7 +136,7 @@ const ImageCropModal: FC<Props> = (props: Props) => {
 
 
   return (
   return (
     <Modal isOpen={isShow} toggle={onModalCloseHandler}>
     <Modal isOpen={isShow} toggle={onModalCloseHandler}>
-      <ModalHeader tag="h4" toggle={onModalCloseHandler} className="bg-info text-light">
+      <ModalHeader tag="h4" toggle={onModalCloseHandler} className="text-info">
         {t('crop_image_modal.image_crop')}
         {t('crop_image_modal.image_crop')}
       </ModalHeader>
       </ModalHeader>
       <ModalBody className="my-4">
       <ModalBody className="my-4">

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

@@ -182,7 +182,7 @@ const CompleteUserRegistrationForm: React.FC<Props> = (props: Props) => {
 
 
               <div className="input-group justify-content-center mt-4">
               <div className="input-group justify-content-center mt-4">
                 <button
                 <button
-                  type="button"
+                  type="submit"
                   disabled={forceDisableForm || disableForm}
                   disabled={forceDisableForm || disableForm}
                   className="btn btn-secondary btn-register col-6 mx-auto d-flex"
                   className="btn btn-secondary btn-register col-6 mx-auto d-flex"
                 >
                 >

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

@@ -86,7 +86,7 @@ export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
 
 
   return (
   return (
     <Modal isOpen={isOpen} toggle={onClose} data-testid="page-template-modal">
     <Modal isOpen={isOpen} toggle={onClose} data-testid="page-template-modal">
-      <ModalHeader tag="h4" toggle={onClose} className="bg-primary text-light">
+      <ModalHeader tag="h4" toggle={onClose}>
         {t('template.modal_label.Create/Edit Template Page')}
         {t('template.modal_label.Create/Edit Template Page')}
       </ModalHeader>
       </ModalHeader>
       <ModalBody>
       <ModalBody>

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

@@ -42,7 +42,7 @@ const DeleteBookmarkFolderModal: FC = () => {
 
 
   return (
   return (
     <Modal size="md" isOpen={isOpened} toggle={closeBookmarkFolderDeleteModal} data-testid="page-delete-modal" className="grw-create-page">
     <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">
+      <ModalHeader tag="h4" toggle={closeBookmarkFolderDeleteModal} className="text-danger">
         <span className="material-symbols-outlined">delete</span>
         <span className="material-symbols-outlined">delete</span>
         {t('bookmark_folder.delete_modal.modal_header_label')}
         {t('bookmark_folder.delete_modal.modal_header_label')}
       </ModalHeader>
       </ModalHeader>

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

@@ -1,5 +1,6 @@
+import type { FC } from 'react';
 import React, {
 import React, {
-  useState, FC,
+  useState,
 } from 'react';
 } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
@@ -60,7 +61,7 @@ const EmptyTrashModal: FC = () => {
 
 
   return (
   return (
     <Modal size="lg" isOpen={isOpened} toggle={closeEmptyTrashModal} data-testid="page-delete-modal">
     <Modal size="lg" isOpen={isOpened} toggle={closeEmptyTrashModal} data-testid="page-delete-modal">
-      <ModalHeader tag="h4" toggle={closeEmptyTrashModal} className="bg-danger text-light">
+      <ModalHeader tag="h4" toggle={closeEmptyTrashModal} className="text-danger">
         <span className="material-symbols-outlined">delete_forever</span>
         <span className="material-symbols-outlined">delete_forever</span>
         {t('modal_empty.empty_the_trash')}
         {t('modal_empty.empty_the_trash')}
       </ModalHeader>
       </ModalHeader>

+ 3 - 56
apps/app/src/components/ItemsTree/ItemsTree.tsx

@@ -8,7 +8,6 @@ import type { Nullable, IPageHasId, IPageToDeleteWithMeta } from '@growi/core';
 import { useGlobalSocket } from '@growi/core/dist/swr';
 import { useGlobalSocket } from '@growi/core/dist/swr';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
-import { debounce } from 'throttle-debounce';
 
 
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import type { IPageForItem } from '~/interfaces/page';
 import type { IPageForItem } from '~/interfaces/page';
@@ -23,7 +22,7 @@ import {
   useSWRxPageAncestorsChildren, useSWRxRootPage, mutatePageTree, mutatePageList,
   useSWRxPageAncestorsChildren, useSWRxRootPage, mutatePageTree, mutatePageList,
 } from '~/stores/page-listing';
 } from '~/stores/page-listing';
 import { mutateSearching } from '~/stores/search';
 import { mutateSearching } from '~/stores/search';
-import { usePageTreeDescCountMap, useSidebarScrollerRef } from '~/stores/ui';
+import { usePageTreeDescCountMap } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { ItemNode, type TreeItemProps } from '../TreeItem';
 import { ItemNode, type TreeItemProps } from '../TreeItem';
@@ -32,6 +31,7 @@ import ItemsTreeContentSkeleton from './ItemsTreeContentSkeleton';
 
 
 import styles from './ItemsTree.module.scss';
 import styles from './ItemsTree.module.scss';
 
 
+const moduleClass = styles['grw-pagetree'] ?? '';
 
 
 const logger = loggerFactory('growi:cli:ItemsTree');
 const logger = loggerFactory('growi:cli:ItemsTree');
 
 
@@ -115,7 +115,6 @@ export const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPagePath } = useCurrentPagePath();
   const { open: openDuplicateModal } = usePageDuplicateModal();
   const { open: openDuplicateModal } = usePageDuplicateModal();
   const { open: openDeleteModal } = usePageDeleteModal();
   const { open: openDeleteModal } = usePageDeleteModal();
-  const { data: sidebarScrollerRef } = useSidebarScrollerRef();
 
 
   const { data: socket } = useGlobalSocket();
   const { data: socket } = useGlobalSocket();
   const { data: ptDescCountMap, update: updatePtDescCountMap } = usePageTreeDescCountMap();
   const { data: ptDescCountMap, update: updatePtDescCountMap } = usePageTreeDescCountMap();
@@ -123,9 +122,6 @@ export const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
   // for mutation
   // for mutation
   const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
   const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
 
 
-  const [isInitialScrollCompleted, setIsInitialScrollCompleted] = useState(false);
-
-  const rootElemRef = useRef(null);
 
 
   const renderingCondition = useMemo(() => {
   const renderingCondition = useMemo(() => {
     return {
     return {
@@ -200,55 +196,6 @@ export const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
     openDeleteModal([pageToDelete], { onDeleted: onDeletedHandler });
     openDeleteModal([pageToDelete], { onDeleted: onDeletedHandler });
   }, [currentPagePath, mutateCurrentPage, openDeleteModal, router, t]);
   }, [currentPagePath, mutateCurrentPage, openDeleteModal, router, t]);
 
 
-  // ***************************  Scroll on init ***************************
-  const scrollOnInit = useCallback(() => {
-    const scrollTargetElement = document.getElementById('grw-pagetree-current-page-item');
-
-    if (sidebarScrollerRef?.current == null || scrollTargetElement == null) {
-      return;
-    }
-
-    logger.debug('scrollOnInit has invoked');
-
-    const scrollElement = sidebarScrollerRef.current.getScrollElement();
-
-    // NOTE: could not use scrollIntoView
-    //  https://stackoverflow.com/questions/11039885/scrollintoview-causing-the-whole-page-to-move
-
-    // calculate the center point
-    const scrollTop = scrollTargetElement.offsetTop - scrollElement.getBoundingClientRect().height / 2;
-    scrollElement.scrollTo({ top: scrollTop });
-
-    setIsInitialScrollCompleted(true);
-  }, [sidebarScrollerRef]);
-
-  const scrollOnInitDebounced = useMemo(() => debounce(500, scrollOnInit), [scrollOnInit]);
-
-  useEffect(() => {
-    if (!isSecondStageRenderingCondition(renderingCondition) || isInitialScrollCompleted) {
-      return;
-    }
-
-    const rootElement = rootElemRef.current as HTMLElement | null;
-    if (rootElement == null) {
-      return;
-    }
-
-    const observerCallback = (mutationRecords: MutationRecord[]) => {
-      mutationRecords.forEach(() => scrollOnInitDebounced());
-    };
-
-    const observer = new MutationObserver(observerCallback);
-    observer.observe(rootElement, { childList: true, subtree: true });
-
-    // first call for the situation that all rendering is complete at this point
-    scrollOnInitDebounced();
-
-    return () => {
-      observer.disconnect();
-    };
-  }, [isInitialScrollCompleted, renderingCondition, scrollOnInitDebounced]);
-  // *******************************  end  *******************************
 
 
   if (error1 != null || error2 != null) {
   if (error1 != null || error2 != null) {
     // TODO: improve message
     // TODO: improve message
@@ -275,7 +222,7 @@ export const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
 
 
   if (initialItemNode != null) {
   if (initialItemNode != null) {
     return (
     return (
-      <ul className={`grw-pagetree ${styles['grw-pagetree']} list-group py-4`} ref={rootElemRef}>
+      <ul className={`grw-pagetree ${moduleClass} list-group py-4`}>
         <CustomTreeItem
         <CustomTreeItem
           key={initialItemNode.page.path}
           key={initialItemNode.page.path}
           targetPathOrId={targetPathOrId}
           targetPathOrId={targetPathOrId}

+ 0 - 51
apps/app/src/components/Layout/Admin.module.scss

@@ -226,49 +226,6 @@ $slack-work-space-name-card-border: #efc1f6;
     }
     }
   }
   }
 
 
-  #layoutOptions {
-    .customize-layout-card {
-      border: 4px solid $border-color;
-    }
-  }
-
-  // theme selector
-  #themeOptions {
-    // layout
-    .theme-option-container {
-      min-width: 100px;
-      a {
-        padding: 3px;
-        margin-right: 10px;
-        margin-bottom: 10px;
-
-        svg {
-          display: block;
-        }
-      }
-    }
-
-    &.disabled {
-      cursor: not-allowed;
-      opacity: 0.5;
-    }
-
-    // style
-    .theme-option-container a {
-      background-color: $gray-100;
-      border: 1px solid $border-color;
-    }
-    .theme-option-name, .theme-option-badge {
-      opacity: 0.3;
-    }
-    // style (active)
-    .theme-option-container.active {
-      .theme-option-name, .theme-option-badge {
-        opacity: 1;
-      }
-    }
-  }
-
   .settings-table {
   .settings-table {
     table-layout: fixed;
     table-layout: fixed;
 
 
@@ -285,14 +242,6 @@ $slack-work-space-name-card-border: #efc1f6;
     }
     }
   }
   }
 
 
-  .admin-navigation {
-    & > a + a {
-      margin-top: 2px;
-    }
-    &.sticky-top {
-      top: 30px;
-    }
-  }
 }
 }
 
 
 
 

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

@@ -56,7 +56,7 @@ const AssociateModal = (props: Props): JSX.Element => {
 
 
   return (
   return (
     <Modal isOpen={isOpen} toggle={closeModalHandler} size="lg" data-testid="grw-associate-modal">
     <Modal isOpen={isOpen} toggle={closeModalHandler} size="lg" data-testid="grw-associate-modal">
-      <ModalHeader className="bg-primary text-light" toggle={onClose}>
+      <ModalHeader toggle={onClose}>
         { t('admin:user_management.create_external_account') }
         { t('admin:user_management.create_external_account') }
       </ModalHeader>
       </ModalHeader>
       <ModalBody>
       <ModalBody>

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

@@ -45,7 +45,7 @@ const DisassociateModal = (props: Props): JSX.Element => {
 
 
   return (
   return (
     <Modal isOpen={props.isOpen} toggle={props.onClose}>
     <Modal isOpen={props.isOpen} toggle={props.onClose}>
-      <ModalHeader className="bg-info text-light" toggle={props.onClose}>
+      <ModalHeader className="text-info" toggle={props.onClose}>
         {t('personal_settings.disassociate_external_account')}
         {t('personal_settings.disassociate_external_account')}
       </ModalHeader>
       </ModalHeader>
       <ModalBody>
       <ModalBody>

+ 1 - 1
apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -309,7 +309,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
 
 
           <nav
           <nav
             className={`${styles['grw-contextual-sub-navigation']}
             className={`${styles['grw-contextual-sub-navigation']}
-              d-flex align-items-center justify-content-end px-2 px-sm-3 px-md-4 py-1 gap-2 gap-md-4 d-print-none
+              d-flex align-items-center justify-content-end pe-2 pe-sm-3 pe-md-4 py-1 gap-2 gap-md-4 d-print-none
             `}
             `}
             data-testid="grw-contextual-sub-nav"
             data-testid="grw-contextual-sub-nav"
             id="grw-contextual-sub-nav"
             id="grw-contextual-sub-nav"

+ 15 - 4
apps/app/src/components/Navbar/PageEditorModeManager.tsx

@@ -1,12 +1,13 @@
-import React, { type ReactNode, useCallback } from 'react';
+import React, { type ReactNode, useCallback, useMemo } from 'react';
 
 
 import { Origin } from '@growi/core';
 import { Origin } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
 import { useCreatePageAndTransit } from '~/client/services/create-page';
 import { useCreatePageAndTransit } from '~/client/services/create-page';
 import { toastError } from '~/client/util/toastr';
 import { toastError } from '~/client/util/toastr';
-import { useIsNotFound, useCurrentPageYjsDraft } from '~/stores/page';
+import { useIsNotFound } from '~/stores/page';
 import { EditorMode, useEditorMode, useIsDeviceLargerThanMd } from '~/stores/ui';
 import { EditorMode, useEditorMode, useIsDeviceLargerThanMd } from '~/stores/ui';
+import { useCurrentPageYjsData } from '~/stores/yjs';
 
 
 import { shouldCreateWipPage } from '../../utils/should-create-wip-page';
 import { shouldCreateWipPage } from '../../utils/should-create-wip-page';
 
 
@@ -64,7 +65,7 @@ export const PageEditorModeManager = (props: Props): JSX.Element => {
   const { data: isNotFound } = useIsNotFound();
   const { data: isNotFound } = useIsNotFound();
   const { mutate: mutateEditorMode } = useEditorMode();
   const { mutate: mutateEditorMode } = useEditorMode();
   const { data: isDeviceLargerThanMd } = useIsDeviceLargerThanMd();
   const { data: isDeviceLargerThanMd } = useIsDeviceLargerThanMd();
-  const { data: currentPageYjsDraft } = useCurrentPageYjsDraft();
+  const { data: currentPageYjsData } = useCurrentPageYjsData();
 
 
   const { isCreating, createAndTransit } = useCreatePageAndTransit();
   const { isCreating, createAndTransit } = useCreatePageAndTransit();
 
 
@@ -87,6 +88,16 @@ export const PageEditorModeManager = (props: Props): JSX.Element => {
 
 
   const _isBtnDisabled = isCreating || isBtnDisabled;
   const _isBtnDisabled = isCreating || isBtnDisabled;
 
 
+  const circleColor = useMemo(() => {
+    if (currentPageYjsData?.awarenessStateSize != null && currentPageYjsData.awarenessStateSize > 0) {
+      return 'bg-primary';
+    }
+
+    if (currentPageYjsData?.hasRevisionBodyDiff != null && currentPageYjsData.hasRevisionBodyDiff) {
+      return 'bg-secondary';
+    }
+  }, [currentPageYjsData]);
+
   return (
   return (
     <>
     <>
       <div
       <div
@@ -113,7 +124,7 @@ export const PageEditorModeManager = (props: Props): JSX.Element => {
             onClick={editButtonClickedHandler}
             onClick={editButtonClickedHandler}
           >
           >
             <span className="material-symbols-outlined me-1 fs-5">edit_square</span>{t('Edit')}
             <span className="material-symbols-outlined me-1 fs-5">edit_square</span>{t('Edit')}
-            { currentPageYjsDraft?.hasYjsDraft && <span className="position-absolute top-0 start-100 translate-middle p-1 bg-primary rounded-circle" />}
+            { circleColor != null && <span className={`position-absolute top-0 start-100 translate-middle p-1 rounded-circle ${circleColor}`} />}
           </PageEditorModeButton>
           </PageEditorModeButton>
         )}
         )}
       </div>
       </div>

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

@@ -4,7 +4,7 @@ import dynamic from 'next/dynamic';
 
 
 import { useHashChangedEffect } from '~/client/services/side-effects/hash-changed';
 import { useHashChangedEffect } from '~/client/services/side-effects/hash-changed';
 import { usePageUpdatedEffect } from '~/client/services/side-effects/page-updated';
 import { usePageUpdatedEffect } from '~/client/services/side-effects/page-updated';
-import { useYjsDraftEffect } from '~/client/services/side-effects/yjs-draft';
+import { useCurrentPageYjsDataEffect } from '~/client/services/side-effects/yjs';
 import { useIsEditable } from '~/stores/context';
 import { useIsEditable } from '~/stores/context';
 import { useIsLatestRevision } from '~/stores/page';
 import { useIsLatestRevision } from '~/stores/page';
 import { EditorMode, useEditorMode } from '~/stores/ui';
 import { EditorMode, useEditorMode } from '~/stores/ui';
@@ -27,7 +27,7 @@ export const DisplaySwitcher = (props: Props): JSX.Element => {
 
 
   usePageUpdatedEffect();
   usePageUpdatedEffect();
   useHashChangedEffect();
   useHashChangedEffect();
-  useYjsDraftEffect();
+  useCurrentPageYjsDataEffect();
 
 
   return (
   return (
     <>
     <>

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

@@ -235,7 +235,7 @@ const FixPageGrantModal = (props: ModalProps): JSX.Element => {
   return (
   return (
     <>
     <>
       <Modal size="lg" isOpen={isOpen} toggle={close}>
       <Modal size="lg" isOpen={isOpen} toggle={close}>
-        <ModalHeader tag="h4" toggle={close} className="bg-primary text-light">
+        <ModalHeader tag="h4" toggle={close}>
           { t('fix_page_grant.modal.title') }
           { t('fix_page_grant.modal.title') }
         </ModalHeader>
         </ModalHeader>
         {renderModalBodyAndFooter()}
         {renderModalBodyAndFooter()}
@@ -245,7 +245,7 @@ const FixPageGrantModal = (props: ModalProps): JSX.Element => {
           isOpen={isGroupSelectModalShown}
           isOpen={isGroupSelectModalShown}
           toggle={() => setIsGroupSelectModalShown(false)}
           toggle={() => setIsGroupSelectModalShown(false)}
         >
         >
-          <ModalHeader tag="h4" toggle={() => setIsGroupSelectModalShown(false)} className="bg-purple text-light">
+          <ModalHeader tag="h4" toggle={() => setIsGroupSelectModalShown(false)}>
             {t('user_group.select_group')}
             {t('user_group.select_group')}
           </ModalHeader>
           </ModalHeader>
           <ModalBody>
           <ModalBody>

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

@@ -101,7 +101,7 @@ export const DeleteAttachmentModal: React.FC = () => {
       aria-labelledby="contained-modal-title-lg"
       aria-labelledby="contained-modal-title-lg"
       fade={false}
       fade={false}
     >
     >
-      <ModalHeader tag="h4" toggle={toggleHandler} className="bg-danger text-light">
+      <ModalHeader tag="h4" toggle={toggleHandler} className="text-danger">
         <span id="contained-modal-title-lg">{t('delete_attachment_modal.confirm_delete_attachment')}</span>
         <span id="contained-modal-title-lg">{t('delete_attachment_modal.confirm_delete_attachment')}</span>
       </ModalHeader>
       </ModalHeader>
       <ModalBody>
       <ModalBody>

+ 1 - 1
apps/app/src/components/PageComment/DeleteCommentModal.tsx

@@ -85,7 +85,7 @@ export const DeleteCommentModal = (props: DeleteCommentModalProps): JSX.Element
 
 
   return (
   return (
     <Modal isOpen={isShown} toggle={cancelToDelete} className={`${styles['page-comment-delete-modal']}`}>
     <Modal isOpen={isShown} toggle={cancelToDelete} className={`${styles['page-comment-delete-modal']}`}>
-      <ModalHeader tag="h4" toggle={cancelToDelete} className="bg-danger text-light">
+      <ModalHeader tag="h4" toggle={cancelToDelete} className="text-danger">
         {headerContent()}
         {headerContent()}
       </ModalHeader>
       </ModalHeader>
       <ModalBody>
       <ModalBody>

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

@@ -139,7 +139,7 @@ const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element =
   const likerIds = isIPageInfoForEntity(pageInfo) ? (pageInfo.likerIds ?? []).slice(0, 15) : [];
   const likerIds = isIPageInfoForEntity(pageInfo) ? (pageInfo.likerIds ?? []).slice(0, 15) : [];
   const seenUserIds = isIPageInfoForEntity(pageInfo) ? (pageInfo.seenUserIds ?? []).slice(0, 15) : [];
   const seenUserIds = isIPageInfoForEntity(pageInfo) ? (pageInfo.seenUserIds ?? []).slice(0, 15) : [];
 
 
-  const { mutateAndSave: mutatePageControlsX } = usePageControlsX();
+  const { mutate: mutatePageControlsX } = usePageControlsX();
 
 
   const pageControlsRef = useRef<HTMLDivElement>(null);
   const pageControlsRef = useRef<HTMLDivElement>(null);
   const [pageControlsRect] = useRect(pageControlsRef);
   const [pageControlsRect] = useRect(pageControlsRef);

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

@@ -292,7 +292,7 @@ const PageCreateModal: React.FC = () => {
       className={`grw-create-page ${styles['grw-create-page']}`}
       className={`grw-create-page ${styles['grw-create-page']}`}
       autoFocus={false}
       autoFocus={false}
     >
     >
-      <ModalHeader tag="h4" toggle={() => closeCreateModal()} className="bg-primary text-light">
+      <ModalHeader tag="h4" toggle={() => closeCreateModal()}>
         {t('New Page')}
         {t('New Page')}
       </ModalHeader>
       </ModalHeader>
       <ModalBody>
       <ModalBody>

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

@@ -279,7 +279,7 @@ const PageDuplicateModal = (): JSX.Element => {
 
 
   return (
   return (
     <Modal size="lg" isOpen={isOpened} toggle={closeDuplicateModal} data-testid="page-duplicate-modal" className="grw-duplicate-page" autoFocus={false}>
     <Modal size="lg" isOpen={isOpened} toggle={closeDuplicateModal} data-testid="page-duplicate-modal" className="grw-duplicate-page" autoFocus={false}>
-      <ModalHeader tag="h4" toggle={closeDuplicateModal} className="bg-primary text-light">
+      <ModalHeader tag="h4" toggle={closeDuplicateModal}>
         { t('modal_duplicate.label.Duplicate page') }
         { t('modal_duplicate.label.Duplicate page') }
       </ModalHeader>
       </ModalHeader>
       <ModalBody>
       <ModalBody>

+ 1 - 1
apps/app/src/components/PageEditor/GridEditModal.jsx

@@ -191,7 +191,7 @@ class GridEditModal extends React.Component {
     const { t } = this.props;
     const { t } = this.props;
     return (
     return (
       <Modal isOpen={this.state.show} toggle={this.cancel} size="xl" className={`grw-grid-edit-modal ${styles['grw-grid-edit-modal']}`}>
       <Modal isOpen={this.state.show} toggle={this.cancel} size="xl" className={`grw-grid-edit-modal ${styles['grw-grid-edit-modal']}`}>
-        <ModalHeader tag="h4" toggle={this.cancel} className="bg-primary text-light">
+        <ModalHeader tag="h4" toggle={this.cancel}>
           {t('grid_edit.create_bootstrap_4_grid')}
           {t('grid_edit.create_bootstrap_4_grid')}
         </ModalHeader>
         </ModalHeader>
         <ModalBody className="container">
         <ModalBody className="container">

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

@@ -338,7 +338,7 @@ export const LinkEditModal = (): JSX.Element => {
 
 
   return (
   return (
     <Modal className="link-edit-modal" isOpen={linkEditModalStatus.isOpened} toggle={close} size="lg" autoFocus={false}>
     <Modal className="link-edit-modal" isOpen={linkEditModalStatus.isOpened} toggle={close} size="lg" autoFocus={false}>
-      <ModalHeader tag="h4" toggle={close} className="bg-primary text-light">
+      <ModalHeader tag="h4" toggle={close}>
         {t('link_edit.edit_link')}
         {t('link_edit.edit_link')}
       </ModalHeader>
       </ModalHeader>
 
 

+ 30 - 3
apps/app/src/components/PageHeader/PageHeader.tsx

@@ -1,6 +1,9 @@
-import type { FC } from 'react';
+import {
+  useCallback, useEffect, useRef, useState,
+} from 'react';
 
 
 import { useSWRxCurrentPage } from '~/stores/page';
 import { useSWRxCurrentPage } from '~/stores/page';
+import { usePageControlsX } from '~/stores/ui';
 
 
 import { PagePathHeader } from './PagePathHeader';
 import { PagePathHeader } from './PagePathHeader';
 import { PageTitleHeader } from './PageTitleHeader';
 import { PageTitleHeader } from './PageTitleHeader';
@@ -9,21 +12,45 @@ import styles from './PageHeader.module.scss';
 
 
 const moduleClass = styles['page-header'] ?? '';
 const moduleClass = styles['page-header'] ?? '';
 
 
-export const PageHeader: FC = () => {
+export const PageHeader = (): JSX.Element => {
+
   const { data: currentPage } = useSWRxCurrentPage();
   const { data: currentPage } = useSWRxCurrentPage();
+  const { data: pageControlsX } = usePageControlsX();
+  const pageHeaderRef = useRef<HTMLDivElement>(null);
+
+  const [maxWidth, setMaxWidth] = useState<number>();
+
+  const calcMaxWidth = useCallback(() => {
+    if (pageControlsX == null || pageHeaderRef.current == null) {
+      // Length that allows users to use PageHeader functionality.
+      setMaxWidth(300);
+      return;
+    }
+    // At least 10px space between PageHeader and PageControls
+    const maxWidth = pageControlsX - pageHeaderRef.current.getBoundingClientRect().x - 10;
+    setMaxWidth(maxWidth);
+  }, [pageControlsX]);
+
+  useEffect(() => {
+    calcMaxWidth();
+  }, [calcMaxWidth]);
 
 
   if (currentPage == null) {
   if (currentPage == null) {
     return <></>;
     return <></>;
   }
   }
 
 
   return (
   return (
-    <div className={`${moduleClass} w-100`}>
+    <div className={`${moduleClass} w-100`} ref={pageHeaderRef}>
       <PagePathHeader
       <PagePathHeader
         currentPage={currentPage}
         currentPage={currentPage}
+        maxWidth={maxWidth}
+        onRenameTerminated={calcMaxWidth}
       />
       />
       <div className="mt-0 mt-md-1">
       <div className="mt-0 mt-md-1">
         <PageTitleHeader
         <PageTitleHeader
           currentPage={currentPage}
           currentPage={currentPage}
+          maxWidth={maxWidth}
+          onMoveTerminated={calcMaxWidth}
         />
         />
       </div>
       </div>
     </div>
     </div>

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

@@ -1,5 +1,4 @@
 .page-path-header :global {
 .page-path-header :global {
-  max-width: calc(100vw - 650px);
   input {
   input {
     min-width: 20px;
     min-width: 20px;
     min-height: unset;
     min-height: unset;
@@ -17,4 +16,9 @@
       transform: translateY(12px);
       transform: translateY(12px);
     }
     }
   }
   }
+
+  // Make Truncated elements horizontally scrollable and hide the scroll bar
+  .page-path-header-input {
+    scrollbar-width: none;
+  }
 }
 }

+ 38 - 38
apps/app/src/components/PageHeader/PagePathHeader.tsx

@@ -1,7 +1,6 @@
 import {
 import {
   useState, useCallback, memo,
   useState, useCallback, memo,
 } from 'react';
 } from 'react';
-import type { FC } from 'react';
 
 
 import type { IPagePopulatedToShowRevision } from '@growi/core';
 import type { IPagePopulatedToShowRevision } from '@growi/core';
 import { DevidedPagePath } from '@growi/core/dist/models';
 import { DevidedPagePath } from '@growi/core/dist/models';
@@ -25,11 +24,15 @@ const moduleClass = styles['page-path-header'];
 type Props = {
 type Props = {
   currentPage: IPagePopulatedToShowRevision,
   currentPage: IPagePopulatedToShowRevision,
   className?: string,
   className?: string,
+  maxWidth?: number,
+  onRenameTerminated?: () => void,
 }
 }
 
 
-export const PagePathHeader: FC<Props> = memo((props: Props) => {
+export const PagePathHeader = memo((props: Props): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
-  const { currentPage, className } = props;
+  const {
+    currentPage, className, maxWidth, onRenameTerminated,
+  } = props;
 
 
   const dPagePath = new DevidedPagePath(currentPage.path, true);
   const dPagePath = new DevidedPagePath(currentPage.path, true);
   const parentPagePath = dPagePath.former;
   const parentPagePath = dPagePath.former;
@@ -38,7 +41,6 @@ export const PagePathHeader: FC<Props> = memo((props: Props) => {
 
 
   const [isRenameInputShown, setRenameInputShown] = useState(false);
   const [isRenameInputShown, setRenameInputShown] = useState(false);
   const [isHover, setHover] = useState(false);
   const [isHover, setHover] = useState(false);
-  const [editingParentPagePath, setEditingParentPagePath] = useState(parentPagePath);
 
 
   // const [isIconHidden, setIsIconHidden] = useState(false);
   // const [isIconHidden, setIsIconHidden] = useState(false);
 
 
@@ -47,34 +49,28 @@ export const PagePathHeader: FC<Props> = memo((props: Props) => {
 
 
   const pagePathRenameHandler = usePagePathRenameHandler(currentPage);
   const pagePathRenameHandler = usePagePathRenameHandler(currentPage);
 
 
-  const onRenameFinish = useCallback(() => {
-    setRenameInputShown(false);
-  }, []);
-
-  const onRenameFailure = useCallback(() => {
-    setRenameInputShown(true);
-  }, []);
 
 
-  const onInputChange = useCallback((inputText: string) => {
-    setEditingParentPagePath(inputText);
-  }, []);
+  const rename = useCallback((inputText) => {
+    const pathToRename = normalizePath(`${inputText}/${dPagePath.latter}`);
+    pagePathRenameHandler(pathToRename,
+      () => {
+        setRenameInputShown(false);
+        onRenameTerminated?.();
+      },
+      () => {
+        setRenameInputShown(true);
+      });
+  }, [dPagePath.latter, pagePathRenameHandler, onRenameTerminated]);
 
 
-  const onPressEnter = useCallback(() => {
-    const pathToRename = normalizePath(`${editingParentPagePath}/${dPagePath.latter}`);
-    pagePathRenameHandler(pathToRename, onRenameFinish, onRenameFailure);
-  }, [editingParentPagePath, onRenameFailure, onRenameFinish, pagePathRenameHandler, dPagePath.latter]);
-
-  const onPressEscape = useCallback(() => {
+  const cancel = useCallback(() => {
     // reset
     // reset
-    setEditingParentPagePath(parentPagePath);
     setRenameInputShown(false);
     setRenameInputShown(false);
-  }, [parentPagePath]);
+  }, []);
 
 
   const onClickEditButton = useCallback(() => {
   const onClickEditButton = useCallback(() => {
     // reset
     // reset
-    setEditingParentPagePath(parentPagePath);
     setRenameInputShown(true);
     setRenameInputShown(true);
-  }, [parentPagePath]);
+  }, []);
 
 
   // TODO: https://redmine.weseek.co.jp/issues/141062
   // TODO: https://redmine.weseek.co.jp/issues/141062
   // Truncate left side and don't use getElementById
   // Truncate left side and don't use getElementById
@@ -104,25 +100,27 @@ export const PagePathHeader: FC<Props> = memo((props: Props) => {
     <div
     <div
       id="page-path-header"
       id="page-path-header"
       className={`d-flex ${moduleClass} ${className ?? ''} small position-relative ms-2`}
       className={`d-flex ${moduleClass} ${className ?? ''} small position-relative ms-2`}
+      style={{ maxWidth }}
       onMouseEnter={() => setHover(true)}
       onMouseEnter={() => setHover(true)}
       onMouseLeave={() => setHover(false)}
       onMouseLeave={() => setHover(false)}
     >
     >
       <div
       <div
-        id="grw-page-path-header-container"
-        className="me-2 d-inline-block overflow-hidden"
+        className="page-path-header-input d-inline-block overflow-x-scroll"
       >
       >
         { isRenameInputShown && (
         { isRenameInputShown && (
-          <div className="position-absolute w-100">
-            <ClosableTextInput
-              value={editingParentPagePath}
-              placeholder={t('Input parent page path')}
-              inputClassName="form-control-sm"
-              onPressEnter={onPressEnter}
-              onPressEscape={onPressEscape}
-              onChange={onInputChange}
-              validationTarget={ValidationTarget.PAGE}
-              onClickOutside={onPressEscape}
-            />
+          <div className="position-relative">
+            <div className="position-absolute w-100">
+              <ClosableTextInput
+                value={parentPagePath}
+                placeholder={t('Input parent page path')}
+                inputClassName="form-control-sm"
+                onPressEnter={rename}
+                onPressEscape={cancel}
+                onBlur={rename}
+                validationTarget={ValidationTarget.PAGE}
+                useAutosizeInput
+              />
+            </div>
           </div>
           </div>
         ) }
         ) }
         <div
         <div
@@ -136,7 +134,9 @@ export const PagePathHeader: FC<Props> = memo((props: Props) => {
         </div>
         </div>
       </div>
       </div>
 
 
-      <div className={`page-path-header-buttons d-flex align-items-center ${isHover && !isRenameInputShown ? '' : 'invisible'}`}>
+      <div
+        className={`page-path-header-buttons d-flex align-items-center ${isHover && !isRenameInputShown ? '' : 'invisible'}`}
+      >
         <button
         <button
           type="button"
           type="button"
           className="btn btn-outline-neutral-secondary me-2 d-flex align-items-center justify-content-center"
           className="btn btn-outline-neutral-secondary me-2 d-flex align-items-center justify-content-center"

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

@@ -1,5 +1,4 @@
 .page-title-header :global {
 .page-title-header :global {
-  max-width: calc(100vw - 650px);
   input {
   input {
     min-width: 20px;
     min-width: 20px;
     min-height: unset;
     min-height: unset;
@@ -7,6 +6,11 @@
     line-height: 1em;
     line-height: 1em;
     transform: translateX(0.05rem) translateY(0.05rem);
     transform: translateX(0.05rem) translateY(0.05rem);
   }
   }
+
+  // Make Truncated elements horizontally scrollable and hide the scroll bar
+  .page-title-header-input {
+    scrollbar-width: none;
+  }
 }
 }
 
 
 .page-title-header-border-color {
 .page-title-header-border-color {

+ 34 - 31
apps/app/src/components/PageHeader/PageTitleHeader.tsx

@@ -1,5 +1,4 @@
-import type { FC } from 'react';
-import { useState, useCallback, useEffect } from 'react';
+import { useState, useCallback } from 'react';
 
 
 import nodePath from 'path';
 import nodePath from 'path';
 
 
@@ -24,11 +23,13 @@ const borderColorClass = styles['page-title-header-border-color'] ?? '';
 type Props = {
 type Props = {
   currentPage: IPagePopulatedToShowRevision,
   currentPage: IPagePopulatedToShowRevision,
   className?: string,
   className?: string,
+  maxWidth?: number,
+  onMoveTerminated?: () => void,
 };
 };
 
 
-export const PageTitleHeader: FC<Props> = (props) => {
+export const PageTitleHeader = (props: Props): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
-  const { currentPage } = props;
+  const { currentPage, maxWidth, onMoveTerminated } = props;
 
 
   const currentPagePath = currentPage.path;
   const currentPagePath = currentPage.path;
 
 
@@ -50,15 +51,7 @@ export const PageTitleHeader: FC<Props> = (props) => {
 
 
   const isNewlyCreatedPage = (currentPage.wip && currentPage.latestRevision == null && untitledPageRegex.test(editedPageTitle)) ?? false;
   const isNewlyCreatedPage = (currentPage.wip && currentPage.latestRevision == null && untitledPageRegex.test(editedPageTitle)) ?? false;
 
 
-  const onRenameFinish = useCallback(() => {
-    setRenameInputShown(false);
-  }, []);
-
-  const onRenameFailure = useCallback(() => {
-    setRenameInputShown(true);
-  }, []);
-
-  const onInputChange = useCallback((inputText: string) => {
+  const inputChangeHandler = useCallback((inputText: string) => {
     const newPageTitle = pathUtils.removeHeadingSlash(inputText);
     const newPageTitle = pathUtils.removeHeadingSlash(inputText);
     const parentPagePath = pathUtils.addTrailingSlash(nodePath.dirname(currentPage.path));
     const parentPagePath = pathUtils.addTrailingSlash(nodePath.dirname(currentPage.path));
     const newPagePath = nodePath.resolve(parentPagePath, newPageTitle);
     const newPagePath = nodePath.resolve(parentPagePath, newPageTitle);
@@ -66,11 +59,18 @@ export const PageTitleHeader: FC<Props> = (props) => {
     setEditedPagePath(newPagePath);
     setEditedPagePath(newPagePath);
   }, [currentPage?.path, setEditedPagePath]);
   }, [currentPage?.path, setEditedPagePath]);
 
 
-  const onPressEnter = useCallback(() => {
-    pagePathRenameHandler(editedPagePath, onRenameFinish, onRenameFailure);
-  }, [editedPagePath, onRenameFailure, onRenameFinish, pagePathRenameHandler]);
-
-  const onPressEscape = useCallback(() => {
+  const rename = useCallback(() => {
+    pagePathRenameHandler(editedPagePath,
+      () => {
+        setRenameInputShown(false);
+        onMoveTerminated?.();
+      },
+      () => {
+        setRenameInputShown(true);
+      });
+  }, [editedPagePath, onMoveTerminated, pagePathRenameHandler]);
+
+  const cancel = useCallback(() => {
     setEditedPagePath(currentPagePath);
     setEditedPagePath(currentPagePath);
     setRenameInputShown(false);
     setRenameInputShown(false);
   }, [currentPagePath]);
   }, [currentPagePath]);
@@ -93,20 +93,23 @@ export const PageTitleHeader: FC<Props> = (props) => {
   // }, [currentPage._id, isNewlyCreatedPage]);
   // }, [currentPage._id, isNewlyCreatedPage]);
 
 
   return (
   return (
-    <div className={`d-flex ${moduleClass} ${props.className ?? ''} position-relative`}>
-      <div className="me-1 d-inline-block overflow-hidden">
+    <div className={`d-flex ${moduleClass} ${props.className ?? ''} position-relative`} style={{ maxWidth }}>
+      <div className="page-title-header-input me-1 d-inline-block overflow-x-scroll">
         { isRenameInputShown && (
         { isRenameInputShown && (
-          <div className="position-absolute w-100">
-            <ClosableTextInput
-              value={isNewlyCreatedPage ? '' : editedPageTitle}
-              placeholder={t('Input page name')}
-              inputClassName="fs-4"
-              onPressEnter={onPressEnter}
-              onPressEscape={onPressEscape}
-              onChange={onInputChange}
-              onClickOutside={() => { setRenameInputShown(false) }}
-              validationTarget={ValidationTarget.PAGE}
-            />
+          <div className="position-relative">
+            <div className="position-absolute w-100">
+              <ClosableTextInput
+                value={isNewlyCreatedPage ? '' : editedPageTitle}
+                placeholder={t('Input page name')}
+                inputClassName="fs-4"
+                onPressEnter={rename}
+                onPressEscape={cancel}
+                onChange={inputChangeHandler}
+                onBlur={rename}
+                validationTarget={ValidationTarget.PAGE}
+                useAutosizeInput
+              />
+            </div>
           </div>
           </div>
         ) }
         ) }
         <h1
         <h1

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

@@ -351,7 +351,7 @@ const PageRenameModal = (): JSX.Element => {
 
 
   return (
   return (
     <Modal size="lg" isOpen={isOpened} toggle={closeRenameModal} data-testid="page-rename-modal" autoFocus={false}>
     <Modal size="lg" isOpen={isOpened} toggle={closeRenameModal} data-testid="page-rename-modal" autoFocus={false}>
-      <ModalHeader tag="h4" toggle={closeRenameModal} className="bg-primary text-light">
+      <ModalHeader tag="h4" toggle={closeRenameModal}>
         { t('modal_rename.label.Move/Rename page') }
         { t('modal_rename.label.Move/Rename page') }
       </ModalHeader>
       </ModalHeader>
       <ModalBody>
       <ModalBody>

+ 13 - 10
apps/app/src/components/PageSelectModal/PageSelectModal.tsx

@@ -1,5 +1,5 @@
 import type { FC } from 'react';
 import type { FC } from 'react';
-import { useState, useCallback } from 'react';
+import { Suspense, useState, useCallback } from 'react';
 
 
 import nodePath from 'path';
 import nodePath from 'path';
 
 
@@ -14,6 +14,7 @@ import { usePageSelectModal } from '~/stores/modal';
 import { useCurrentPagePath, useCurrentPageId, useSWRxCurrentPage } from '~/stores/page';
 import { useCurrentPagePath, useCurrentPageId, useSWRxCurrentPage } from '~/stores/page';
 
 
 import { ItemsTree } from '../ItemsTree';
 import { ItemsTree } from '../ItemsTree';
+import ItemsTreeContentSkeleton from '../ItemsTree/ItemsTreeContentSkeleton';
 import { usePagePathRenameHandler } from '../PageEditor/page-path-rename-utils';
 import { usePagePathRenameHandler } from '../PageEditor/page-path-rename-utils';
 
 
 import { TreeItemForModal } from './TreeItemForModal';
 import { TreeItemForModal } from './TreeItemForModal';
@@ -83,15 +84,17 @@ export const PageSelectModal: FC = () => {
     >
     >
       <ModalHeader toggle={closeModal}>{t('page_select_modal.select_page_location')}</ModalHeader>
       <ModalHeader toggle={closeModal}>{t('page_select_modal.select_page_location')}</ModalHeader>
       <ModalBody>
       <ModalBody>
-        <ItemsTree
-          CustomTreeItem={TreeItemForModal}
-          isEnableActions={!isGuestUser}
-          isReadOnlyUser={!!isReadOnlyUser}
-          targetPath={path}
-          targetPathOrId={targetPathOrId}
-          targetAndAncestorsData={targetAndAncestorsData}
-          onClickTreeItem={onClickTreeItem}
-        />
+        <Suspense fallback={<ItemsTreeContentSkeleton />}>
+          <ItemsTree
+            CustomTreeItem={TreeItemForModal}
+            isEnableActions={!isGuestUser}
+            isReadOnlyUser={!!isReadOnlyUser}
+            targetPath={path}
+            targetPathOrId={targetPathOrId}
+            targetAndAncestorsData={targetAndAncestorsData}
+            onClickTreeItem={onClickTreeItem}
+          />
+        </Suspense>
       </ModalBody>
       </ModalBody>
       <ModalFooter>
       <ModalFooter>
         <Button color="secondary" onClick={onClickCancel}>{t('Cancel')}</Button>
         <Button color="secondary" onClick={onClickCancel}>{t('Cancel')}</Button>

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

@@ -151,7 +151,7 @@ const ConvertByPathModal = React.memo((props: ConvertByPathModalProps): JSX.Elem
 
 
   return (
   return (
     <Modal size="lg" isOpen={props.isOpen} toggle={props.close}>
     <Modal size="lg" isOpen={props.isOpen} toggle={props.close}>
-      <ModalHeader tag="h4" toggle={props.close} className="bg-primary text-light">
+      <ModalHeader tag="h4" toggle={props.close}>
         { t('private_legacy_pages.by_path_modal.title') }
         { t('private_legacy_pages.by_path_modal.title') }
       </ModalHeader>
       </ModalHeader>
       <ModalBody>
       <ModalBody>

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

@@ -74,7 +74,7 @@ export const PrivateLegacyPagesMigrationModal = (): JSX.Element => {
 
 
   return (
   return (
     <Modal size="lg" isOpen={isOpened} toggle={close}>
     <Modal size="lg" isOpen={isOpened} toggle={close}>
-      <ModalHeader tag="h4" toggle={close} className="bg-primary text-light">
+      <ModalHeader tag="h4" toggle={close}>
         { t('private_legacy_pages.modal.title') }
         { t('private_legacy_pages.modal.title') }
       </ModalHeader>
       </ModalHeader>
       <ModalBody>
       <ModalBody>

+ 1 - 1
apps/app/src/components/PutbackPageModal.jsx

@@ -115,7 +115,7 @@ const PutBackPageModal = () => {
 
 
   return (
   return (
     <Modal isOpen={isOpened} toggle={closeModalHandler} data-testid="put-back-page-modal">
     <Modal isOpen={isOpened} toggle={closeModalHandler} data-testid="put-back-page-modal">
-      <ModalHeader tag="h4" toggle={closeModalHandler} className="bg-info text-light">
+      <ModalHeader tag="h4" toggle={closeModalHandler} className="text-info">
         <HeaderContent />
         <HeaderContent />
       </ModalHeader>
       </ModalHeader>
       <ModalBody>
       <ModalBody>

+ 1 - 1
apps/app/src/components/SearchPage/SearchOptionModal.tsx

@@ -47,7 +47,7 @@ const SearchOptionModal: FC<Props> = (props: Props) => {
 
 
   return (
   return (
     <Modal size="lg" isOpen={isOpen} toggle={onCloseModal} autoFocus={false}>
     <Modal size="lg" isOpen={isOpen} toggle={onCloseModal} autoFocus={false}>
-      <ModalHeader tag="h4" toggle={onCloseModal} className="bg-primary text-light">
+      <ModalHeader tag="h4" toggle={onCloseModal}>
         Search Option
         Search Option
       </ModalHeader>
       </ModalHeader>
       <ModalBody>
       <ModalBody>

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

@@ -163,7 +163,7 @@ const ShortcutsModal = (): JSX.Element => {
     <>
     <>
       { status != null && (
       { status != null && (
         <Modal id="shortcuts-modal" size="lg" isOpen={status.isOpened} toggle={close} className={`shortcuts-modal ${styles['shortcuts-modal']}`}>
         <Modal id="shortcuts-modal" size="lg" isOpen={status.isOpened} toggle={close} className={`shortcuts-modal ${styles['shortcuts-modal']}`}>
-          <ModalHeader tag="h4" toggle={close} className="bg-primary text-light">
+          <ModalHeader tag="h4" toggle={close}>
             {t('Shortcuts')}
             {t('Shortcuts')}
           </ModalHeader>
           </ModalHeader>
           <ModalBody>
           <ModalBody>

+ 12 - 7
apps/app/src/components/Sidebar/Bookmarks/BookmarkContents.tsx

@@ -2,7 +2,7 @@ import React, { useCallback, useState } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
-import { apiv3Post } from '~/client/util/apiv3-client';
+import { addNewFolder } from '~/client/util/bookmark-utils';
 import { toastError } from '~/client/util/toastr';
 import { toastError } from '~/client/util/toastr';
 import { BookmarkFolderNameInput } from '~/components/Bookmarks/BookmarkFolderNameInput';
 import { BookmarkFolderNameInput } from '~/components/Bookmarks/BookmarkFolderNameInput';
 import { BookmarkFolderTree } from '~/components/Bookmarks/BookmarkFolderTree';
 import { BookmarkFolderTree } from '~/components/Bookmarks/BookmarkFolderTree';
@@ -21,20 +21,24 @@ export const BookmarkContents = (): JSX.Element => {
     setIsCreateAction(true);
     setIsCreateAction(true);
   }, []);
   }, []);
 
 
-  const onClickonClickOutsideHandler = useCallback(() => {
+  const cancel = useCallback(() => {
     setIsCreateAction(false);
     setIsCreateAction(false);
   }, []);
   }, []);
 
 
-  const onPressEnterHandlerForCreate = useCallback(async(folderName: string) => {
+  const create = useCallback(async(folderName: string) => {
+    if (folderName.trim() === '') {
+      return cancel();
+    }
+
     try {
     try {
-      await apiv3Post('/bookmark-folder', { name: folderName, parent: null });
+      await addNewFolder(folderName.trim(), null);
       await mutateBookmarkFolders();
       await mutateBookmarkFolders();
       setIsCreateAction(false);
       setIsCreateAction(false);
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
     }
     }
-  }, [mutateBookmarkFolders]);
+  }, [cancel, mutateBookmarkFolders]);
 
 
   return (
   return (
     <div>
     <div>
@@ -54,8 +58,9 @@ export const BookmarkContents = (): JSX.Element => {
       {isCreateAction && (
       {isCreateAction && (
         <div className="col-12 mb-2 ">
         <div className="col-12 mb-2 ">
           <BookmarkFolderNameInput
           <BookmarkFolderNameInput
-            onClickOutside={onClickonClickOutsideHandler}
-            onPressEnter={onPressEnterHandlerForCreate}
+            onPressEnter={create}
+            onBlur={create}
+            onPressEscape={cancel}
           />
           />
         </div>
         </div>
       )}
       )}

+ 72 - 5
apps/app/src/components/Sidebar/PageTree/PageTreeSubstance.tsx

@@ -1,13 +1,20 @@
-import React, { memo, useCallback } from 'react';
+import React, {
+  memo, useCallback, useEffect, useMemo, useRef, useState,
+} from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import {
 import {
   UncontrolledButtonDropdown, DropdownMenu, DropdownToggle, DropdownItem,
   UncontrolledButtonDropdown, DropdownMenu, DropdownToggle, DropdownItem,
 } from 'reactstrap';
 } from 'reactstrap';
+import { debounce } from 'throttle-debounce';
 
 
 import { useTargetAndAncestors, useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
 import { useTargetAndAncestors, useIsGuestUser, useIsReadOnlyUser } from '~/stores/context';
 import { useCurrentPagePath, useCurrentPageId } from '~/stores/page';
 import { useCurrentPagePath, useCurrentPageId } from '~/stores/page';
-import { mutatePageTree, useSWRxRootPage, useSWRxV5MigrationStatus } from '~/stores/page-listing';
+import {
+  mutatePageTree, useSWRxPageAncestorsChildren, useSWRxRootPage, useSWRxV5MigrationStatus,
+} from '~/stores/page-listing';
+import { useSidebarScrollerRef } from '~/stores/ui';
+import loggerFactory from '~/utils/logger';
 
 
 import { ItemsTree } from '../../ItemsTree/ItemsTree';
 import { ItemsTree } from '../../ItemsTree/ItemsTree';
 import { PageTreeItem } from '../PageTreeItem';
 import { PageTreeItem } from '../PageTreeItem';
@@ -15,6 +22,8 @@ import { SidebarHeaderReloadButton } from '../SidebarHeaderReloadButton';
 
 
 import { PrivateLegacyPagesLink } from './PrivateLegacyPagesLink';
 import { PrivateLegacyPagesLink } from './PrivateLegacyPagesLink';
 
 
+const logger = loggerFactory('growi:cli:PageTreeSubstance');
+
 type HeaderProps = {
 type HeaderProps = {
   isWipPageShown: boolean,
   isWipPageShown: boolean,
   onWipPageShownChange?: () => void
   onWipPageShownChange?: () => void
@@ -91,6 +100,65 @@ export const PageTreeContent = memo(({ isWipPageShown }: PageTreeContentProps) =
   const { data: migrationStatus } = useSWRxV5MigrationStatus({ suspense: true });
   const { data: migrationStatus } = useSWRxV5MigrationStatus({ suspense: true });
 
 
   const targetPathOrId = targetId || currentPath;
   const targetPathOrId = targetId || currentPath;
+  const path = currentPath || '/';
+
+  const { data: ancestorsChildrenResult } = useSWRxPageAncestorsChildren(path, { suspense: true });
+  const { data: rootPageResult } = useSWRxRootPage({ suspense: true });
+  const { data: sidebarScrollerRef } = useSidebarScrollerRef();
+  const [isInitialScrollCompleted, setIsInitialScrollCompleted] = useState(false);
+
+  const rootElemRef = useRef(null);
+
+  // ***************************  Scroll on init ***************************
+  const scrollOnInit = useCallback(() => {
+    const scrollTargetElement = document.getElementById('grw-pagetree-current-page-item');
+
+    if (sidebarScrollerRef?.current == null || scrollTargetElement == null) {
+      return;
+    }
+
+    logger.debug('scrollOnInit has invoked');
+
+    const scrollElement = sidebarScrollerRef.current;
+
+    // NOTE: could not use scrollIntoView
+    //  https://stackoverflow.com/questions/11039885/scrollintoview-causing-the-whole-page-to-move
+
+    // calculate the center point
+    const scrollTop = scrollTargetElement.offsetTop - scrollElement.getBoundingClientRect().height / 2;
+    scrollElement.scrollTo({ top: scrollTop });
+
+    setIsInitialScrollCompleted(true);
+  }, [sidebarScrollerRef]);
+
+  const scrollOnInitDebounced = useMemo(() => debounce(500, scrollOnInit), [scrollOnInit]);
+
+  useEffect(() => {
+    if (isInitialScrollCompleted || ancestorsChildrenResult == null || rootPageResult == null) {
+      return;
+    }
+
+    const rootElement = rootElemRef.current as HTMLElement | null;
+    if (rootElement == null) {
+      return;
+    }
+
+    const observerCallback = (mutationRecords: MutationRecord[]) => {
+      mutationRecords.forEach(() => scrollOnInitDebounced());
+    };
+
+    const observer = new MutationObserver(observerCallback);
+    observer.observe(rootElement, { childList: true, subtree: true });
+
+    // first call for the situation that all rendering is complete at this point
+    scrollOnInitDebounced();
+
+    return () => {
+      observer.disconnect();
+    };
+  }, [isInitialScrollCompleted, scrollOnInitDebounced, ancestorsChildrenResult, rootPageResult]);
+  // *******************************  end  *******************************
+
 
 
   if (!migrationStatus?.isV5Compatible) {
   if (!migrationStatus?.isV5Compatible) {
     return <PageTreeUnavailable />;
     return <PageTreeUnavailable />;
@@ -103,10 +171,9 @@ export const PageTreeContent = memo(({ isWipPageShown }: PageTreeContentProps) =
     return null;
     return null;
   }
   }
 
 
-  const path = currentPath || '/';
 
 
   return (
   return (
-    <>
+    <div ref={rootElemRef}>
       <ItemsTree
       <ItemsTree
         isEnableActions={!isGuestUser}
         isEnableActions={!isGuestUser}
         isReadOnlyUser={!!isReadOnlyUser}
         isReadOnlyUser={!!isReadOnlyUser}
@@ -124,7 +191,7 @@ export const PageTreeContent = memo(({ isWipPageShown }: PageTreeContentProps) =
           </div>
           </div>
         </div>
         </div>
       )}
       )}
-    </>
+    </div>
   );
   );
 });
 });
 
 

+ 14 - 7
apps/app/src/components/Sidebar/PageTreeItem/Ellipsis.tsx

@@ -66,7 +66,15 @@ export const Ellipsis: FC<TreeItemToolProps> = (props) => {
     setRenameInputShown(true);
     setRenameInputShown(true);
   }, []);
   }, []);
 
 
-  const onPressEnterForRenameHandler = async(inputText: string) => {
+  const cancel = useCallback(() => {
+    setRenameInputShown(false);
+  }, []);
+
+  const rename = useCallback(async(inputText) => {
+    if (inputText.trim() === '') {
+      return cancel();
+    }
+
     const parentPath = pathUtils.addTrailingSlash(nodePath.dirname(page.path ?? ''));
     const parentPath = pathUtils.addTrailingSlash(nodePath.dirname(page.path ?? ''));
     const newPagePath = nodePath.resolve(parentPath, inputText);
     const newPagePath = nodePath.resolve(parentPath, inputText);
 
 
@@ -83,9 +91,7 @@ export const Ellipsis: FC<TreeItemToolProps> = (props) => {
         newPagePath,
         newPagePath,
       });
       });
 
 
-      if (onRenamed != null) {
-        onRenamed(page.path, newPagePath);
-      }
+      onRenamed?.(page.path, newPagePath);
 
 
       toastSuccess(t('renamed_pages', { path: page.path }));
       toastSuccess(t('renamed_pages', { path: page.path }));
     }
     }
@@ -93,7 +99,7 @@ export const Ellipsis: FC<TreeItemToolProps> = (props) => {
       setRenameInputShown(true);
       setRenameInputShown(true);
       toastError(err);
       toastError(err);
     }
     }
-  };
+  }, [cancel, onRenamed, page._id, page.path, page.revision, t]);
 
 
   const deleteMenuItemClickHandler = useCallback(async(_pageId: string, pageInfo: IPageInfoAll | undefined): Promise<void> => {
   const deleteMenuItemClickHandler = useCallback(async(_pageId: string, pageInfo: IPageInfoAll | undefined): Promise<void> => {
     if (onClickDeleteMenuItem == null) {
     if (onClickDeleteMenuItem == null) {
@@ -136,8 +142,9 @@ export const Ellipsis: FC<TreeItemToolProps> = (props) => {
             <ClosableTextInput
             <ClosableTextInput
               value={nodePath.basename(page.path ?? '')}
               value={nodePath.basename(page.path ?? '')}
               placeholder={t('Input page name')}
               placeholder={t('Input page name')}
-              onClickOutside={() => { setRenameInputShown(false) }}
-              onPressEnter={onPressEnterForRenameHandler}
+              onPressEnter={rename}
+              onBlur={rename}
+              onPressEscape={cancel}
               validationTarget={ValidationTarget.PAGE}
               validationTarget={ValidationTarget.PAGE}
             />
             />
           </NotDraggableForClosableTextInput>
           </NotDraggableForClosableTextInput>

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

@@ -8,9 +8,6 @@ import {
 import { DevidedPagePath } from '@growi/core/dist/models';
 import { DevidedPagePath } from '@growi/core/dist/models';
 import { UserPicture } from '@growi/ui/dist/components';
 import { UserPicture } from '@growi/ui/dist/components';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
-import {
-  DropdownItem, DropdownMenu, DropdownToggle, UncontrolledButtonDropdown,
-} from 'reactstrap';
 
 
 import { useKeywordManager } from '~/client/services/search-operation';
 import { useKeywordManager } from '~/client/services/search-operation';
 import { PagePathHierarchicalLink } from '~/components/Common/PagePathHierarchicalLink';
 import { PagePathHierarchicalLink } from '~/components/Common/PagePathHierarchicalLink';
@@ -182,13 +179,20 @@ export const RecentChangesHeader = ({
     <>
     <>
       <SidebarHeaderReloadButton onClick={() => mutate()} />
       <SidebarHeaderReloadButton onClick={() => mutate()} />
 
 
-      <UncontrolledButtonDropdown className="me-1">
-        <DropdownToggle color="transparent" className="p-0 border-0">
+      <div className="me-1">
+        <button
+          color="transparent"
+          className="btn p-0 border-0"
+          type="button"
+          data-bs-toggle="dropdown"
+          data-bs-auto-close="outside"
+          aria-expanded="false"
+        >
           <span className="material-symbols-outlined">more_horiz</span>
           <span className="material-symbols-outlined">more_horiz</span>
-        </DropdownToggle>
+        </button>
 
 
-        <DropdownMenu container="body">
-          <DropdownItem onClick={changeSizeHandler}>
+        <ul className="dropdown-menu">
+          <li className="dropdown-item" onClick={changeSizeHandler}>
             <div className={`${styles['grw-recent-changes-resize-button']} form-check form-switch mb-0`}>
             <div className={`${styles['grw-recent-changes-resize-button']} form-check form-switch mb-0`}>
               <input
               <input
                 id="recentChangesResize"
                 id="recentChangesResize"
@@ -201,9 +205,9 @@ export const RecentChangesHeader = ({
                 {isSmall ? t('sidebar_header.size_s') : t('sidebar_header.size_l')}
                 {isSmall ? t('sidebar_header.size_s') : t('sidebar_header.size_l')}
               </label>
               </label>
             </div>
             </div>
-          </DropdownItem>
+          </li>
 
 
-          <DropdownItem onClick={onWipPageShownChange}>
+          <li className="dropdown-item" onClick={onWipPageShownChange}>
             <div className="form-check form-switch mb-0">
             <div className="form-check form-switch mb-0">
               <input
               <input
                 id="wipPageVisibility"
                 id="wipPageVisibility"
@@ -216,9 +220,9 @@ export const RecentChangesHeader = ({
                 {t('sidebar_header.show_wip_page')}
                 {t('sidebar_header.show_wip_page')}
               </label>
               </label>
             </div>
             </div>
-          </DropdownItem>
-        </DropdownMenu>
-      </UncontrolledButtonDropdown>
+          </li>
+        </ul>
+      </div>
     </>
     </>
   );
   );
 };
 };

+ 7 - 0
apps/app/src/components/Sidebar/Sidebar.tsx

@@ -1,6 +1,7 @@
 import React, {
 import React, {
   type FC,
   type FC,
   memo, useCallback, useEffect, useState,
   memo, useCallback, useEffect, useState,
+  useRef,
 } from 'react';
 } from 'react';
 
 
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
@@ -13,6 +14,7 @@ import {
   useCurrentProductNavWidth,
   useCurrentProductNavWidth,
   usePreferCollapsedMode,
   usePreferCollapsedMode,
   useSidebarMode,
   useSidebarMode,
+  useSidebarScrollerRef,
 } from '~/stores/ui';
 } from '~/stores/ui';
 
 
 import { DrawerToggler } from '../Common/DrawerToggler';
 import { DrawerToggler } from '../Common/DrawerToggler';
@@ -109,6 +111,10 @@ const CollapsibleContainer = memo((props: CollapsibleContainerProps): JSX.Elemen
   const { data: currentProductNavWidth } = useCurrentProductNavWidth();
   const { data: currentProductNavWidth } = useCurrentProductNavWidth();
   const { data: isCollapsedContentsOpened, mutate: mutateCollapsedContentsOpened } = useCollapsedContentsOpened();
   const { data: isCollapsedContentsOpened, mutate: mutateCollapsedContentsOpened } = useCollapsedContentsOpened();
 
 
+  const sidebarScrollerRef = useRef<HTMLDivElement>(null);
+  const { mutate: mutateSidebarScroller } = useSidebarScrollerRef();
+  mutateSidebarScroller(sidebarScrollerRef);
+
 
 
   // open menu when collapsed mode
   // open menu when collapsed mode
   const primaryItemHoverHandler = useCallback(() => {
   const primaryItemHoverHandler = useCallback(() => {
@@ -138,6 +144,7 @@ const CollapsibleContainer = memo((props: CollapsibleContainerProps): JSX.Elemen
     <div className={`flex-expand-horiz ${className}`} onMouseLeave={mouseLeaveHandler}>
     <div className={`flex-expand-horiz ${className}`} onMouseLeave={mouseLeaveHandler}>
       <Nav onPrimaryItemHover={primaryItemHoverHandler} />
       <Nav onPrimaryItemHover={primaryItemHoverHandler} />
       <div
       <div
+        ref={sidebarScrollerRef}
         className={`sidebar-contents-container flex-grow-1 overflow-y-auto overflow-x-hidden ${closedClass} ${openedClass}`}
         className={`sidebar-contents-container flex-grow-1 overflow-y-auto overflow-x-hidden ${closedClass} ${openedClass}`}
         style={{ width: collapsibleContentsWidth }}
         style={{ width: collapsibleContentsWidth }}
       >
       >

+ 3 - 2
apps/app/src/components/Sidebar/SidebarNav/SecondaryItems.tsx

@@ -4,7 +4,7 @@ import { memo } from 'react';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import Link from 'next/link';
 import Link from 'next/link';
 
 
-import { useGrowiCloudUri, useIsAdmin } from '~/stores/context';
+import { useIsGuestUser, useGrowiCloudUri, useIsAdmin } from '~/stores/context';
 
 
 import { SkeletonItem } from './SkeletonItem';
 import { SkeletonItem } from './SkeletonItem';
 
 
@@ -43,10 +43,11 @@ export const SecondaryItems: FC = memo(() => {
 
 
   const { data: isAdmin } = useIsAdmin();
   const { data: isAdmin } = useIsAdmin();
   const { data: growiCloudUri } = useGrowiCloudUri();
   const { data: growiCloudUri } = useGrowiCloudUri();
+  const { data: isGuestUser } = useIsGuestUser();
 
 
   return (
   return (
     <div className={styles['grw-secondary-items']}>
     <div className={styles['grw-secondary-items']}>
-      <PersonalDropdown />
+      {!isGuestUser && <PersonalDropdown />}
       <SecondaryItem label="Help" iconName="help" href={growiCloudUri != null ? 'https://growi.cloud/help/' : 'https://docs.growi.org'} isBlank />
       <SecondaryItem label="Help" iconName="help" href={growiCloudUri != null ? 'https://growi.cloud/help/' : 'https://docs.growi.org'} isBlank />
       {isAdmin && <SecondaryItem label="Admin" iconName="settings" href="/admin" />}
       {isAdmin && <SecondaryItem label="Admin" iconName="settings" href="/admin" />}
       <SecondaryItem label="Trash" href="/trash" iconName="delete" />
       <SecondaryItem label="Trash" href="/trash" iconName="delete" />

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

@@ -177,7 +177,7 @@ const TemplateModalSubstance = (props: TemplateModalSubstanceProps): JSX.Element
 
 
   return (
   return (
     <div data-testid="template-modal">
     <div data-testid="template-modal">
-      <ModalHeader tag="h4" toggle={close} className="bg-primary text-light">
+      <ModalHeader tag="h4" toggle={close}>
         {t('template.modal_label.Select template')}
         {t('template.modal_label.Select template')}
       </ModalHeader>
       </ModalHeader>
       <ModalBody className="container">
       <ModalBody className="container">

+ 1 - 1
apps/app/src/components/TreeItem/ItemNode.ts

@@ -1,4 +1,4 @@
-import { IPageForItem } from '../../interfaces/page';
+import type { IPageForItem } from '../../interfaces/page';
 
 
 export class ItemNode {
 export class ItemNode {
 
 

+ 16 - 18
apps/app/src/components/TreeItem/NewPageInput/NewPageInput.tsx

@@ -1,4 +1,6 @@
-import React, { type FC, useCallback, useEffect } from 'react';
+import React, {
+  type FC, useCallback,
+} from 'react';
 
 
 import nodePath from 'path';
 import nodePath from 'path';
 
 
@@ -29,7 +31,15 @@ export const NewPageInput: FC<Props> = (props) => {
     onCanceled,
     onCanceled,
   } = props;
   } = props;
 
 
-  const onPressEnterForCreateHandler = async(inputText: string) => {
+  const cancel = useCallback(() => {
+    onCanceled?.();
+  }, [onCanceled]);
+
+  const create = useCallback(async(inputText) => {
+    if (inputText.trim() === '') {
+      return cancel();
+    }
+
     const parentPath = pathUtils.addTrailingSlash(page.path as string);
     const parentPath = pathUtils.addTrailingSlash(page.path as string);
     const newPagePath = nodePath.resolve(parentPath, inputText);
     const newPagePath = nodePath.resolve(parentPath, inputText);
     const isCreatable = pagePathUtils.isCreatablePage(newPagePath);
     const isCreatable = pagePathUtils.isCreatablePage(newPagePath);
@@ -49,20 +59,7 @@ export const NewPageInput: FC<Props> = (props) => {
     finally {
     finally {
       onSubmittionFailed?.();
       onSubmittionFailed?.();
     }
     }
-  };
-
-  const onPressEscHandler = useCallback((event) => {
-    if (event.keyCode === 27) {
-      onCanceled?.();
-    }
-  }, [onCanceled]);
-
-  useEffect(() => {
-    document.addEventListener('keydown', onPressEscHandler, false);
-    return () => {
-      document.removeEventListener('keydown', onPressEscHandler, false);
-    };
-  }, [onPressEscHandler]);
+  }, [cancel, onSubmit, onSubmittionFailed, page.path, t]);
 
 
   return (
   return (
     <>
     <>
@@ -70,8 +67,9 @@ export const NewPageInput: FC<Props> = (props) => {
         <NotDraggableForClosableTextInput>
         <NotDraggableForClosableTextInput>
           <ClosableTextInput
           <ClosableTextInput
             placeholder={t('Input page name')}
             placeholder={t('Input page name')}
-            onClickOutside={onCanceled}
-            onPressEnter={onPressEnterForCreateHandler}
+            onPressEnter={create}
+            onPressEscape={cancel}
+            onBlur={create}
             validationTarget={ValidationTarget.PAGE}
             validationTarget={ValidationTarget.PAGE}
           />
           />
         </NotDraggableForClosableTextInput>
         </NotDraggableForClosableTextInput>

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

@@ -151,7 +151,7 @@ export const SyncExecution = ({
         isOpen={isAlertModalOpen}
         isOpen={isAlertModalOpen}
         toggle={() => setIsAlertModalOpen(false)}
         toggle={() => setIsAlertModalOpen(false)}
       >
       >
-        <ModalHeader tag="h4" toggle={() => setIsAlertModalOpen(false)} className="bg-purple text-light">
+        <ModalHeader tag="h4" toggle={() => setIsAlertModalOpen(false)} className="text-info">
           <span className="material-symbols-outlined me-1 align-middle">error</span>
           <span className="material-symbols-outlined me-1 align-middle">error</span>
           <span className="align-middle">{t('external_user_group.confirmation_before_sync')}</span>
           <span className="align-middle">{t('external_user_group.confirmation_before_sync')}</span>
         </ModalHeader>
         </ModalHeader>

+ 1 - 1
apps/app/src/features/growi-plugin/client/components/Admin/PluginsExtensionPageContents/PluginDeleteModal.tsx

@@ -42,7 +42,7 @@ export const PluginDeleteModal: React.FC = () => {
 
 
   return (
   return (
     <Modal isOpen={isOpen} toggle={toggleHandler}>
     <Modal isOpen={isOpen} toggle={toggleHandler}>
-      <ModalHeader tag="h4" toggle={toggleHandler} className="bg-danger text-light" name={name}>
+      <ModalHeader tag="h4" toggle={toggleHandler} className="text-danger" name={name}>
         <span>
         <span>
           <span className="material-symbols-outlined">delete_forever</span>
           <span className="material-symbols-outlined">delete_forever</span>
           {t('plugins.confirm')}
           {t('plugins.confirm')}

+ 0 - 8
apps/app/src/interfaces/page.ts

@@ -75,11 +75,3 @@ export type IOptionsForCreate = {
   origin?: Origin
   origin?: Origin
   wip?: boolean,
   wip?: boolean,
 };
 };
-
-export type CurrentPageYjsDraft = {
-  hasYjsDraft: boolean,
-}
-
-export const CurrentPageYjsDraftData = {
-  hasYjsDraft: { hasYjsDraft: true },
-};

+ 1 - 0
apps/app/src/interfaces/sidebar-config.ts

@@ -1,4 +1,5 @@
 
 
 export interface ISidebarConfig {
 export interface ISidebarConfig {
   isSidebarCollapsedMode: boolean,
   isSidebarCollapsedMode: boolean,
+  isSidebarClosedAtDockMode?: boolean,
 }
 }

+ 0 - 1
apps/app/src/interfaces/user-ui-settings.ts

@@ -2,7 +2,6 @@ import type { SidebarContentsType } from './ui';
 
 
 export interface IUserUISettings {
 export interface IUserUISettings {
   currentSidebarContents: SidebarContentsType,
   currentSidebarContents: SidebarContentsType,
-  currentPageControlsX: number,
   currentProductNavWidth: number,
   currentProductNavWidth: number,
   preferCollapsedModeByUser: boolean,
   preferCollapsedModeByUser: boolean,
 }
 }

+ 2 - 1
apps/app/src/interfaces/websocket.ts

@@ -50,7 +50,8 @@ export const SocketEventName = {
   PageDeleted: 'page:delete',
   PageDeleted: 'page:delete',
 
 
   // Yjs
   // Yjs
-  YjsUpdated: 'yjsDraft:update',
+  YjsAwarenessStateSizeUpdated: 'yjs:awareness-state-size-update',
+  YjsHasRevisionBodyDiffUpdated: 'yjs:has-revision-body-diff-update',
 } as const;
 } as const;
 export type SocketEventName = typeof SocketEventName[keyof typeof SocketEventName];
 export type SocketEventName = typeof SocketEventName[keyof typeof SocketEventName];
 
 

+ 4 - 0
apps/app/src/interfaces/yjs.ts

@@ -0,0 +1,4 @@
+export type CurrentPageYjsData = {
+  hasRevisionBodyDiff?: boolean,
+  awarenessStateSize?: number,
+}

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

@@ -26,6 +26,8 @@ import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
 import { SupportedAction, type SupportedActionType } from '~/interfaces/activity';
 import { SupportedAction, type SupportedActionType } from '~/interfaces/activity';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { RendererConfig } from '~/interfaces/services/renderer';
+import type { ISidebarConfig } from '~/interfaces/sidebar-config';
+import type { CurrentPageYjsData } from '~/interfaces/yjs';
 import type { PageModel, PageDocument } from '~/server/models/page';
 import type { PageModel, PageDocument } from '~/server/models/page';
 import type { PageRedirectModel } from '~/server/models/page-redirect';
 import type { PageRedirectModel } from '~/server/models/page-redirect';
 import {
 import {
@@ -42,12 +44,13 @@ import {
 } from '~/stores/context';
 } from '~/stores/context';
 import { useEditingMarkdown } from '~/stores/editor';
 import { useEditingMarkdown } from '~/stores/editor';
 import {
 import {
-  useSWRxCurrentPage, useSWRMUTxCurrentPage, useCurrentPageId, useCurrentPageYjsDraft,
+  useSWRxCurrentPage, useSWRMUTxCurrentPage, useCurrentPageId,
   useIsNotFound, useIsLatestRevision, useTemplateTagData, useTemplateBodyData,
   useIsNotFound, useIsLatestRevision, useTemplateTagData, useTemplateBodyData,
 } from '~/stores/page';
 } from '~/stores/page';
 import { useRedirectFrom } from '~/stores/page-redirect';
 import { useRedirectFrom } from '~/stores/page-redirect';
 import { useRemoteRevisionId } from '~/stores/remote-latest-page';
 import { useRemoteRevisionId } from '~/stores/remote-latest-page';
 import { useSetupGlobalSocket, useSetupGlobalSocketForPage } from '~/stores/websocket';
 import { useSetupGlobalSocket, useSetupGlobalSocketForPage } from '~/stores/websocket';
+import { useCurrentPageYjsData, useSWRMUTxCurrentPageYjsData } from '~/stores/yjs';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { BasicLayout } from '../components/Layout/BasicLayout';
 import { BasicLayout } from '../components/Layout/BasicLayout';
@@ -148,6 +151,8 @@ type Props = CommonProps & {
   isSearchScopeChildrenAsDefault: boolean,
   isSearchScopeChildrenAsDefault: boolean,
   isEnabledMarp: boolean,
   isEnabledMarp: boolean,
 
 
+  sidebarConfig: ISidebarConfig,
+
   isSlackConfigured: boolean,
   isSlackConfigured: boolean,
   // isMailerSetup: boolean,
   // isMailerSetup: boolean,
   isAclEnabled: boolean,
   isAclEnabled: boolean,
@@ -169,7 +174,7 @@ type Props = CommonProps & {
   skipSSR: boolean,
   skipSSR: boolean,
   ssrMaxRevisionBodyLength: number,
   ssrMaxRevisionBodyLength: number,
 
 
-  yjsDraft?: string
+  yjsData: CurrentPageYjsData,
 
 
   rendererConfig: RendererConfig,
   rendererConfig: RendererConfig,
 };
 };
@@ -221,8 +226,6 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   useIsUploadAllFileAllowed(props.isUploadAllFileAllowed);
   useIsUploadAllFileAllowed(props.isUploadAllFileAllowed);
   useIsUploadEnabled(props.isUploadEnabled);
   useIsUploadEnabled(props.isUploadEnabled);
 
 
-  useCurrentPageYjsDraft({ hasYjsDraft: props.yjsDraft != null });
-
   const { pageWithMeta } = props;
   const { pageWithMeta } = props;
 
 
   const pageId = pageWithMeta?.data._id;
   const pageId = pageWithMeta?.data._id;
@@ -233,6 +236,8 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   const { data: currentPage } = useSWRxCurrentPage(pageWithMeta?.data ?? null); // store initial data
   const { data: currentPage } = useSWRxCurrentPage(pageWithMeta?.data ?? null); // store initial data
 
 
   const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
   const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
+  const { trigger: mutateCurrentPageYjsDataFromApi } = useSWRMUTxCurrentPageYjsData();
+
   const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
   const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
   const { data: currentPageId, mutate: mutateCurrentPageId } = useCurrentPageId();
   const { data: currentPageId, mutate: mutateCurrentPageId } = useCurrentPageId();
 
 
@@ -245,6 +250,8 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
   const { mutate: mutateTemplateTagData } = useTemplateTagData();
   const { mutate: mutateTemplateTagData } = useTemplateTagData();
   const { mutate: mutateTemplateBodyData } = useTemplateBodyData();
   const { mutate: mutateTemplateBodyData } = useTemplateBodyData();
 
 
+  const { mutate: mutateCurrentPageYjsData } = useCurrentPageYjsData();
+
   useSetupGlobalSocket();
   useSetupGlobalSocket();
   useSetupGlobalSocketForPage(pageId);
   useSetupGlobalSocketForPage(pageId);
 
 
@@ -257,14 +264,15 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
     if (currentPageId != null && !props.isNotFound) {
     if (currentPageId != null && !props.isNotFound) {
       const mutatePageData = async() => {
       const mutatePageData = async() => {
         const pageData = await mutateCurrentPage();
         const pageData = await mutateCurrentPage();
-        mutateEditingMarkdown(props.yjsDraft ?? pageData?.revision?.body);
+        mutateEditingMarkdown(pageData?.revision?.body);
+        mutateCurrentPageYjsDataFromApi();
       };
       };
 
 
       // If skipSSR is true, use the API to retrieve page data.
       // If skipSSR is true, use the API to retrieve page data.
       // Because pageWIthMeta does not contain revision.body
       // Because pageWIthMeta does not contain revision.body
       mutatePageData();
       mutatePageData();
     }
     }
-  }, [currentPageId, mutateCurrentPage, mutateEditingMarkdown, props.isNotFound, props.skipSSR, props.yjsDraft]);
+  }, [currentPageId, mutateCurrentPage, mutateCurrentPageYjsDataFromApi, mutateEditingMarkdown, props.isNotFound, props.skipSSR]);
 
 
   // sync pathname by Shallow Routing https://nextjs.org/docs/routing/shallow-routing
   // sync pathname by Shallow Routing https://nextjs.org/docs/routing/shallow-routing
   useEffect(() => {
   useEffect(() => {
@@ -277,11 +285,11 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
 
 
   // initialize mutateEditingMarkdown only once per page
   // initialize mutateEditingMarkdown only once per page
   // need to include useCurrentPathname not useCurrentPagePath
   // need to include useCurrentPathname not useCurrentPagePath
-  useEffect(() => {
-    if (props.currentPathname != null) {
-      mutateEditingMarkdown(props.yjsDraft ?? revisionBody);
-    }
-  }, [mutateEditingMarkdown, revisionBody, props.currentPathname, props.yjsDraft]);
+  // useEffect(() => {
+  //   if (props.currentPathname != null) {
+  //     mutateEditingMarkdown(props.yjsDraft ?? revisionBody);
+  //   }
+  // }, [mutateEditingMarkdown, revisionBody, props.currentPathname, props.yjsDraft]);
 
 
   useEffect(() => {
   useEffect(() => {
     mutateRemoteRevisionId(pageWithMeta?.data.revision?._id);
     mutateRemoteRevisionId(pageWithMeta?.data.revision?._id);
@@ -307,6 +315,10 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
     mutateTemplateBodyData(props.templateBodyData);
     mutateTemplateBodyData(props.templateBodyData);
   }, [props.templateBodyData, mutateTemplateBodyData]);
   }, [props.templateBodyData, mutateTemplateBodyData]);
 
 
+  useEffect(() => {
+    mutateCurrentPageYjsData(props.yjsData);
+  }, [mutateCurrentPageYjsData, props.yjsData]);
+
   // If the data on the page changes without router.push, pageWithMeta remains old because getServerSideProps() is not executed
   // If the data on the page changes without router.push, pageWithMeta remains old because getServerSideProps() is not executed
   // So preferentially take page data from useSWRxCurrentPage
   // So preferentially take page data from useSWRxCurrentPage
   const pagePath = currentPage?.path ?? pageWithMeta?.data.path ?? props.currentPathname;
   const pagePath = currentPage?.path ?? pageWithMeta?.data.path ?? props.currentPathname;
@@ -487,7 +499,9 @@ async function injectRoutingInformation(context: GetServerSidePropsContext, prop
       }
       }
     }
     }
 
 
-    props.yjsDraft = crowi.pageService.getYjsDraft(page._id);
+    if (!props.skipSSR) {
+      props.yjsData = await crowi.pageService.getYjsData(page._id);
+    }
   }
   }
 }
 }
 
 
@@ -535,6 +549,11 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
 
 
   props.isEnabledAttachTitleHeader = configManager.getConfig('crowi', 'customize:isEnabledAttachTitleHeader');
   props.isEnabledAttachTitleHeader = configManager.getConfig('crowi', 'customize:isEnabledAttachTitleHeader');
 
 
+  props.sidebarConfig = {
+    isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
+    isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
+  };
+
   props.rendererConfig = {
   props.rendererConfig = {
     isEnabledLinebreaks: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks'),
     isEnabledLinebreaks: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks'),
     isEnabledLinebreaksInComments: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments'),
     isEnabledLinebreaksInComments: configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments'),

+ 3 - 0
apps/app/src/pages/_private-legacy-pages.page.tsx

@@ -10,6 +10,7 @@ import Head from 'next/head';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { RendererConfig } from '~/interfaces/services/renderer';
+import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 import {
 import {
   useCsrfToken, useCurrentUser, useIsSearchPage, useIsSearchScopeChildrenAsDefault,
   useCsrfToken, useCurrentUser, useIsSearchPage, useIsSearchScopeChildrenAsDefault,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useRendererConfig, useGrowiCloudUri, useIsEnabledMarp, useCurrentPathname,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useRendererConfig, useGrowiCloudUri, useIsEnabledMarp, useCurrentPathname,
@@ -34,6 +35,7 @@ type Props = CommonProps & {
   // Render config
   // Render config
   rendererConfig: RendererConfig,
   rendererConfig: RendererConfig,
 
 
+  sidebarConfig: ISidebarConfig,
 };
 };
 
 
 const PrivateLegacyPage: NextPage<Props> = (props: Props) => {
 const PrivateLegacyPage: NextPage<Props> = (props: Props) => {
@@ -97,6 +99,7 @@ async function injectServerConfigurations(context: GetServerSidePropsContext, pr
 
 
   props.sidebarConfig = {
   props.sidebarConfig = {
     isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
     isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
+    isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
   };
   };
 
 
   props.rendererConfig = {
   props.rendererConfig = {

+ 5 - 1
apps/app/src/pages/_search.page.tsx

@@ -11,6 +11,7 @@ import SearchResultLayout from '~/components/Layout/SearchResultLayout';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { RendererConfig } from '~/interfaces/services/renderer';
+import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 import {
 import {
   useCsrfToken, useCurrentUser, useIsContainerFluid, useIsSearchPage, useIsSearchScopeChildrenAsDefault,
   useCsrfToken, useCurrentUser, useIsContainerFluid, useIsSearchPage, useIsSearchScopeChildrenAsDefault,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useRendererConfig, useShowPageLimitationL, useGrowiCloudUri, useCurrentPathname,
   useIsSearchServiceConfigured, useIsSearchServiceReachable, useRendererConfig, useShowPageLimitationL, useGrowiCloudUri, useCurrentPathname,
@@ -42,6 +43,7 @@ type Props = CommonProps & {
 
 
   isContainerFluid: boolean,
   isContainerFluid: boolean,
 
 
+  sidebarConfig: ISidebarConfig,
 };
 };
 
 
 const SearchResultPage: NextPageWithLayout<Props> = (props: Props) => {
 const SearchResultPage: NextPageWithLayout<Props> = (props: Props) => {
@@ -88,7 +90,8 @@ const SearchResultPage: NextPageWithLayout<Props> = (props: Props) => {
 };
 };
 
 
 type LayoutProps = Props & {
 type LayoutProps = Props & {
-  children?: ReactNode
+  sidebarConfig: ISidebarConfig,
+  children?: ReactNode,
 }
 }
 
 
 const Layout = ({ children, ...props }: LayoutProps): JSX.Element => {
 const Layout = ({ children, ...props }: LayoutProps): JSX.Element => {
@@ -123,6 +126,7 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
 
 
   props.sidebarConfig = {
   props.sidebarConfig = {
     isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
     isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
+    isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
   };
   };
 
 
   props.rendererConfig = {
   props.rendererConfig = {

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

@@ -13,6 +13,7 @@ import { BasicLayout } from '~/components/Layout/BasicLayout';
 import { GroundGlassBar } from '~/components/Navbar/GroundGlassBar';
 import { GroundGlassBar } from '~/components/Navbar/GroundGlassBar';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { RendererConfig } from '~/interfaces/services/renderer';
+import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 import {
 import {
   useCurrentUser, useIsSearchPage, useGrowiCloudUri,
   useCurrentUser, useIsSearchPage, useGrowiCloudUri,
   useIsSearchServiceConfigured, useIsSearchServiceReachable,
   useIsSearchServiceConfigured, useIsSearchServiceReachable,
@@ -41,6 +42,8 @@ type Props = CommonProps & {
 
 
   // config
   // config
   registrationWhitelist: string[],
   registrationWhitelist: string[],
+
+  sidebarConfig: ISidebarConfig,
 };
 };
 
 
 const PersonalSettings = dynamic(() => import('~/components/Me/PersonalSettings'), { ssr: false });
 const PersonalSettings = dynamic(() => import('~/components/Me/PersonalSettings'), { ssr: false });
@@ -178,6 +181,7 @@ async function injectServerConfigurations(context: GetServerSidePropsContext, pr
 
 
   props.sidebarConfig = {
   props.sidebarConfig = {
     isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
     isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
+    isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
   };
   };
 
 
   props.rendererConfig = {
   props.rendererConfig = {

+ 4 - 0
apps/app/src/pages/tags.page.tsx

@@ -12,6 +12,7 @@ import Head from 'next/head';
 import { GroundGlassBar } from '~/components/Navbar/GroundGlassBar';
 import { GroundGlassBar } from '~/components/Navbar/GroundGlassBar';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { RendererConfig } from '~/interfaces/services/renderer';
+import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 import type { IDataTagCount } from '~/interfaces/tag';
 import type { IDataTagCount } from '~/interfaces/tag';
 import { useCurrentPageId, useSWRxCurrentPage } from '~/stores/page';
 import { useCurrentPageId, useSWRxCurrentPage } from '~/stores/page';
 import { useSWRxTagsList } from '~/stores/tag';
 import { useSWRxTagsList } from '~/stores/tag';
@@ -38,6 +39,8 @@ type Props = CommonProps & {
   isSearchScopeChildrenAsDefault: boolean,
   isSearchScopeChildrenAsDefault: boolean,
 
 
   rendererConfig: RendererConfig,
   rendererConfig: RendererConfig,
+
+  sidebarConfig: ISidebarConfig,
 };
 };
 
 
 const TagList = dynamic(() => import('~/components/TagList'), { ssr: false });
 const TagList = dynamic(() => import('~/components/TagList'), { ssr: false });
@@ -153,6 +156,7 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
 
 
   props.sidebarConfig = {
   props.sidebarConfig = {
     isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
     isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
+    isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
   };
   };
 
 
 }
 }

+ 4 - 0
apps/app/src/pages/trash.page.tsx

@@ -11,6 +11,7 @@ import { PagePathNavSticky } from '~/components/Common/PagePathNav';
 import { GroundGlassBar } from '~/components/Navbar/GroundGlassBar';
 import { GroundGlassBar } from '~/components/Navbar/GroundGlassBar';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { RendererConfig } from '~/interfaces/services/renderer';
+import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 import { useCurrentPageId, useSWRxCurrentPage } from '~/stores/page';
 import { useCurrentPageId, useSWRxCurrentPage } from '~/stores/page';
 
 
 import { BasicLayout } from '../components/Layout/BasicLayout';
 import { BasicLayout } from '../components/Layout/BasicLayout';
@@ -38,6 +39,8 @@ type Props = CommonProps & {
   showPageLimitationXL: number,
   showPageLimitationXL: number,
 
 
   rendererConfig: RendererConfig,
   rendererConfig: RendererConfig,
+
+  sidebarConfig: ISidebarConfig,
 };
 };
 
 
 const TrashPage: NextPageWithLayout<CommonProps> = (props: Props) => {
 const TrashPage: NextPageWithLayout<CommonProps> = (props: Props) => {
@@ -119,6 +122,7 @@ function injectServerConfigurations(context: GetServerSidePropsContext, props: P
 
 
   props.sidebarConfig = {
   props.sidebarConfig = {
     isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
     isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
+    isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
   };
   };
 
 
 }
 }

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

@@ -1,4 +1,4 @@
-import { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
+import type { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import Head from 'next/head';
 import Head from 'next/head';
@@ -8,10 +8,11 @@ import { NoLoginLayout } from '~/components/Layout/NoLoginLayout';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { UserActivationErrorCode } from '~/interfaces/errors/user-activation';
 import type { UserActivationErrorCode } from '~/interfaces/errors/user-activation';
 import type { RegistrationMode } from '~/interfaces/registration-mode';
 import type { RegistrationMode } from '~/interfaces/registration-mode';
-import { IUserRegistrationOrder } from '~/server/models/user-registration-order';
+import type { ReqWithUserRegistrationOrder } from '~/server/middlewares/inject-user-registration-order-by-token-middleware';
 
 
+import type { CommonProps } from './utils/commons';
 import {
 import {
-  getServerSideCommonProps, getNextI18NextConfig, generateCustomTitle, CommonProps,
+  getServerSideCommonProps, getNextI18NextConfig, generateCustomTitle,
 } from './utils/commons';
 } from './utils/commons';
 
 
 type Props = CommonProps & {
 type Props = CommonProps & {
@@ -56,18 +57,17 @@ async function injectNextI18NextConfigurations(context: GetServerSidePropsContex
 
 
 export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
 export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
   const result = await getServerSideCommonProps(context);
   const result = await getServerSideCommonProps(context);
-  const req: CrowiRequest = context.req as CrowiRequest;
+  const req = context.req as ReqWithUserRegistrationOrder & CrowiRequest;
 
 
   // check for presence
   // check for presence
   // see: https://github.com/vercel/next.js/issues/19271#issuecomment-730006862
   // see: https://github.com/vercel/next.js/issues/19271#issuecomment-730006862
   if (!('props' in result)) {
   if (!('props' in result)) {
     throw new Error('invalid getSSP result');
     throw new Error('invalid getSSP result');
   }
   }
-
   const props: Props = result.props as Props;
   const props: Props = result.props as Props;
 
 
-  if (context.query.userRegistrationOrder != null) {
-    const userRegistrationOrder = context.query.userRegistrationOrder as unknown as IUserRegistrationOrder;
+  if (req.userRegistrationOrder != null) {
+    const userRegistrationOrder = req.userRegistrationOrder;
     props.email = userRegistrationOrder.email;
     props.email = userRegistrationOrder.email;
     props.token = userRegistrationOrder.token;
     props.token = userRegistrationOrder.token;
   }
   }
@@ -75,7 +75,6 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
   if (typeof context.query.errorCode === 'string') {
   if (typeof context.query.errorCode === 'string') {
     props.errorCode = context.query.errorCode as UserActivationErrorCode;
     props.errorCode = context.query.errorCode as UserActivationErrorCode;
   }
   }
-
   props.registrationMode = req.crowi.configManager.getConfig('crowi', 'security:registrationMode');
   props.registrationMode = req.crowi.configManager.getConfig('crowi', 'security:registrationMode');
   props.isEmailAuthenticationEnabled = req.crowi.configManager.getConfig('crowi', 'security:passport-local:isEmailAuthenticationEnabled');
   props.isEmailAuthenticationEnabled = req.crowi.configManager.getConfig('crowi', 'security:passport-local:isEmailAuthenticationEnabled');
 
 

+ 0 - 4
apps/app/src/pages/utils/commons.ts

@@ -36,7 +36,6 @@ export type CommonProps = {
   isAccessDeniedForNonAdminUser?: boolean,
   isAccessDeniedForNonAdminUser?: boolean,
   currentUser?: IUserHasId,
   currentUser?: IUserHasId,
   forcedColorScheme?: ColorScheme,
   forcedColorScheme?: ColorScheme,
-  sidebarConfig: ISidebarConfig,
   userUISettings?: IUserUISettings
   userUISettings?: IUserUISettings
 } & Partial<SSRConfig>;
 } & Partial<SSRConfig>;
 
 
@@ -101,9 +100,6 @@ export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(c
     isDefaultLogo,
     isDefaultLogo,
     forcedColorScheme,
     forcedColorScheme,
     growiCloudUri: configManager.getConfig('crowi', 'app:growiCloudUri'),
     growiCloudUri: configManager.getConfig('crowi', 'app:growiCloudUri'),
-    sidebarConfig: {
-      isSidebarCollapsedMode: configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
-    },
     userUISettings: userUISettings?.toObject?.() ?? userUISettings,
     userUISettings: userUISettings?.toObject?.() ?? userUISettings,
   };
   };
 
 

+ 3 - 1
apps/app/src/server/models/config.ts

@@ -1,5 +1,6 @@
 import { PresetThemes } from '@growi/preset-themes';
 import { PresetThemes } from '@growi/preset-themes';
-import { Types, Schema } from 'mongoose';
+import type { Types } from 'mongoose';
+import { Schema } from 'mongoose';
 import uniqueValidator from 'mongoose-unique-validator';
 import uniqueValidator from 'mongoose-unique-validator';
 
 
 import { RehypeSanitizeOption } from '../../interfaces/rehype';
 import { RehypeSanitizeOption } from '../../interfaces/rehype';
@@ -136,6 +137,7 @@ export const defaultCrowiConfigs: { [key: string]: any } = {
   'customize:isSearchScopeChildrenAsDefault': false,
   'customize:isSearchScopeChildrenAsDefault': false,
   'customize:isEnabledMarp': false,
   'customize:isEnabledMarp': false,
   'customize:isSidebarCollapsedMode': false,
   'customize:isSidebarCollapsedMode': false,
+  'customize:isSidebarClosedAtDockMode': false,
 
 
   'notification:owner-page:isEnabled': false,
   'notification:owner-page:isEnabled': false,
   'notification:group-page:isEnabled': false,
   'notification:group-page:isEnabled': false,

+ 0 - 3
apps/app/src/server/models/user-ui-settings.ts

@@ -23,9 +23,6 @@ const schema = new Schema<UserUISettingsDocument, UserUISettingsModel>({
     enum: SidebarContentsType,
     enum: SidebarContentsType,
     default: SidebarContentsType.RECENT,
     default: SidebarContentsType.RECENT,
   },
   },
-  currentPageControlsX: {
-    type: Number,
-  },
   currentProductNavWidth: { type: Number },
   currentProductNavWidth: { type: Number },
   preferCollapsedModeByUser: { type: Boolean, default: false },
   preferCollapsedModeByUser: { type: Boolean, default: false },
 });
 });

+ 5 - 1
apps/app/src/server/routes/apiv3/customize-setting.js

@@ -113,6 +113,7 @@ module.exports = (crowi) => {
     ],
     ],
     sidebar: [
     sidebar: [
       body('isSidebarCollapsedMode').isBoolean(),
       body('isSidebarCollapsedMode').isBoolean(),
+      body('isSidebarClosedAtDockMode').optional().isBoolean(),
     ],
     ],
     function: [
     function: [
       body('isEnabledTimeline').isBoolean(),
       body('isEnabledTimeline').isBoolean(),
@@ -342,7 +343,8 @@ module.exports = (crowi) => {
 
 
     try {
     try {
       const isSidebarCollapsedMode = await crowi.configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode');
       const isSidebarCollapsedMode = await crowi.configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode');
-      return res.apiv3({ isSidebarCollapsedMode });
+      const isSidebarClosedAtDockMode = await crowi.configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode');
+      return res.apiv3({ isSidebarCollapsedMode, isSidebarClosedAtDockMode });
     }
     }
     catch (err) {
     catch (err) {
       const msg = 'Error occurred in getting sidebar';
       const msg = 'Error occurred in getting sidebar';
@@ -354,12 +356,14 @@ module.exports = (crowi) => {
   router.put('/sidebar', loginRequiredStrictly, adminRequired, validator.sidebar, apiV3FormValidator, addActivity, async(req, res) => {
   router.put('/sidebar', loginRequiredStrictly, adminRequired, validator.sidebar, apiV3FormValidator, addActivity, async(req, res) => {
     const requestParams = {
     const requestParams = {
       'customize:isSidebarCollapsedMode': req.body.isSidebarCollapsedMode,
       'customize:isSidebarCollapsedMode': req.body.isSidebarCollapsedMode,
+      'customize:isSidebarClosedAtDockMode': req.body.isSidebarClosedAtDockMode,
     };
     };
 
 
     try {
     try {
       await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
       await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
       const customizedParams = {
       const customizedParams = {
         isSidebarCollapsedMode: await crowi.configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
         isSidebarCollapsedMode: await crowi.configManager.getConfig('crowi', 'customize:isSidebarCollapsedMode'),
+        isSidebarClosedAtDockMode: await crowi.configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
       };
       };
 
 
       activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SIDEBAR_UPDATE });
       activityEvent.emit('update', res.locals.activity._id, { action: SupportedAction.ACTION_ADMIN_SIDEBAR_UPDATE });

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

@@ -22,6 +22,7 @@ import {
 import type { PageDocument, PageModel } from '~/server/models/page';
 import type { PageDocument, PageModel } from '~/server/models/page';
 import PageTagRelation from '~/server/models/page-tag-relation';
 import PageTagRelation from '~/server/models/page-tag-relation';
 import { configManager } from '~/server/service/config-manager';
 import { configManager } from '~/server/service/config-manager';
+import { getTranslation } from '~/server/service/i18next';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { apiV3FormValidator } from '../../../middlewares/apiv3-form-validator';
 import { apiV3FormValidator } from '../../../middlewares/apiv3-form-validator';
@@ -43,8 +44,9 @@ async function generateUntitledPath(parentPath: string, basePathname: string, in
 }
 }
 
 
 async function determinePath(_parentPath?: string, _path?: string, optionalParentPath?: string): Promise<string> {
 async function determinePath(_parentPath?: string, _path?: string, optionalParentPath?: string): Promise<string> {
-  // TODO: https://redmine.weseek.co.jp/issues/142729
-  const basePathname = 'Untitled';
+  const { t } = await getTranslation();
+
+  const basePathname = t?.('create_page.untitled') || 'Untitled';
 
 
   if (_path != null) {
   if (_path != null) {
     const path = normalizePath(_path);
     const path = normalizePath(_path);

+ 57 - 0
apps/app/src/server/routes/apiv3/page/get-yjs-data.ts

@@ -0,0 +1,57 @@
+import type { IPage, IUserHasId } from '@growi/core';
+import { ErrorV3 } from '@growi/core/dist/models';
+import type { Request, RequestHandler } from 'express';
+import type { ValidationChain } from 'express-validator';
+import { param } from 'express-validator';
+import mongoose from 'mongoose';
+
+import type Crowi from '~/server/crowi';
+import type { PageModel } from '~/server/models/page';
+import loggerFactory from '~/utils/logger';
+
+import { apiV3FormValidator } from '../../../middlewares/apiv3-form-validator';
+import type { ApiV3Response } from '../interfaces/apiv3-response';
+
+
+const logger = loggerFactory('growi:routes:apiv3:page:get-yjs-data');
+
+type GetYjsDataHandlerFactory = (crowi: Crowi) => RequestHandler[];
+
+type ReqParams = {
+  pageId: string,
+}
+interface Req extends Request<ReqParams, ApiV3Response> {
+  user: IUserHasId,
+}
+export const getYjsDataHandlerFactory: GetYjsDataHandlerFactory = (crowi) => {
+  const Page = mongoose.model<IPage, PageModel>('Page');
+  const accessTokenParser = require('../../../middlewares/access-token-parser')(crowi);
+  const loginRequiredStrictly = require('../../../middlewares/login-required')(crowi);
+
+  // define validators for req.params
+  const validator: ValidationChain[] = [
+    param('pageId').isMongoId().withMessage('The param "pageId" must be specified'),
+  ];
+
+  return [
+    accessTokenParser, loginRequiredStrictly,
+    validator, apiV3FormValidator,
+    async(req: Req, res: ApiV3Response) => {
+      const { pageId } = req.params;
+
+      // check whether accessible
+      if (!(await Page.isAccessiblePageByViewer(pageId, req.user))) {
+        return res.apiv3Err(new ErrorV3('Current user is not accessible to this page.', 'forbidden-page'), 403);
+      }
+
+      try {
+        const yjsData = await crowi.pageService.getYjsData(pageId);
+        return res.apiv3({ yjsData });
+      }
+      catch (err) {
+        logger.error(err);
+        return res.apiv3Err(err);
+      }
+    },
+  ];
+};

+ 3 - 0
apps/app/src/server/routes/apiv3/page/index.ts

@@ -24,6 +24,7 @@ import loggerFactory from '~/utils/logger';
 
 
 import { checkPageExistenceHandlersFactory } from './check-page-existence';
 import { checkPageExistenceHandlersFactory } from './check-page-existence';
 import { createPageHandlersFactory } from './create-page';
 import { createPageHandlersFactory } from './create-page';
+import { getYjsDataHandlerFactory } from './get-yjs-data';
 import { publishPageHandlersFactory } from './publish-page';
 import { publishPageHandlersFactory } from './publish-page';
 import { unpublishPageHandlersFactory } from './unpublish-page';
 import { unpublishPageHandlersFactory } from './unpublish-page';
 import { updatePageHandlersFactory } from './update-page';
 import { updatePageHandlersFactory } from './update-page';
@@ -908,5 +909,7 @@ module.exports = (crowi) => {
 
 
   router.put('/:pageId/unpublish', unpublishPageHandlersFactory(crowi));
   router.put('/:pageId/unpublish', unpublishPageHandlersFactory(crowi));
 
 
+  router.get('/:pageId/yjs-data', getYjsDataHandlerFactory(crowi));
+
   return router;
   return router;
 };
 };

+ 2 - 2
apps/app/src/server/routes/user-activation.ts

@@ -1,7 +1,7 @@
-import { Response, NextFunction } from 'express';
+import type { Response, NextFunction } from 'express';
 
 
 import type { UserActivationErrorCode } from '~/interfaces/errors/user-activation';
 import type { UserActivationErrorCode } from '~/interfaces/errors/user-activation';
-import { ReqWithUserRegistrationOrder } from '~/server/middlewares/inject-user-registration-order-by-token-middleware';
+import type { ReqWithUserRegistrationOrder } from '~/server/middlewares/inject-user-registration-order-by-token-middleware';
 
 
 type Crowi = {
 type Crowi = {
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   // eslint-disable-next-line @typescript-eslint/no-explicit-any

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików